Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 7a94acb

Browse files
authored
feat: support self-signed jwt in requests and urllib3 transports (#679)
1 parent bf5ce0c commit 7a94acb

8 files changed

Lines changed: 177 additions & 1 deletion

File tree

google/auth/transport/requests.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from google.auth import exceptions
4646
from google.auth import transport
4747
import google.auth.transport._mtls_helper
48+
from google.oauth2 import service_account
4849

4950
_LOGGER = logging.getLogger(__name__)
5051

@@ -313,6 +314,9 @@ def my_cert_callback():
313314
refreshing credentials. If not passed,
314315
an instance of :class:`~google.auth.transport.requests.Request`
315316
is created.
317+
default_host (Optional[str]): A host like "pubsub.googleapis.com".
318+
This is used when a self-signed JWT is created from service
319+
account credentials.
316320
"""
317321

318322
def __init__(
@@ -322,13 +326,15 @@ def __init__(
322326
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
323327
refresh_timeout=None,
324328
auth_request=None,
329+
default_host=None,
325330
):
326331
super(AuthorizedSession, self).__init__()
327332
self.credentials = credentials
328333
self._refresh_status_codes = refresh_status_codes
329334
self._max_refresh_attempts = max_refresh_attempts
330335
self._refresh_timeout = refresh_timeout
331336
self._is_mtls = False
337+
self._default_host = default_host
332338

333339
if auth_request is None:
334340
auth_request_session = requests.Session()
@@ -347,6 +353,17 @@ def __init__(
347353
# credentials.refresh).
348354
self._auth_request = auth_request
349355

356+
# https://google.aip.dev/auth/4111
357+
# Attempt to use self-signed JWTs when a service account is used.
358+
# A default host must be explicitly provided.
359+
if (
360+
isinstance(self.credentials, service_account.Credentials)
361+
and self._default_host
362+
):
363+
self.credentials._create_self_signed_jwt(
364+
"https://{}/".format(self._default_host)
365+
)
366+
350367
def configure_mtls_channel(self, client_cert_callback=None):
351368
"""Configure the client certificate and key for SSL connection.
352369

google/auth/transport/urllib3.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from google.auth import environment_vars
5050
from google.auth import exceptions
5151
from google.auth import transport
52+
from google.oauth2 import service_account
5253

5354
_LOGGER = logging.getLogger(__name__)
5455

@@ -262,6 +263,9 @@ def my_cert_callback():
262263
retried.
263264
max_refresh_attempts (int): The maximum number of times to attempt to
264265
refresh the credentials and retry the request.
266+
default_host (Optional[str]): A host like "pubsub.googleapis.com".
267+
This is used when a self-signed JWT is created from service
268+
account credentials.
265269
"""
266270

267271
def __init__(
@@ -270,6 +274,7 @@ def __init__(
270274
http=None,
271275
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
272276
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
277+
default_host=None,
273278
):
274279
if http is None:
275280
self.http = _make_default_http()
@@ -281,10 +286,22 @@ def __init__(
281286
self.credentials = credentials
282287
self._refresh_status_codes = refresh_status_codes
283288
self._max_refresh_attempts = max_refresh_attempts
289+
self._default_host = default_host
284290
# Request instance used by internal methods (for example,
285291
# credentials.refresh).
286292
self._request = Request(self.http)
287293

294+
# https://google.aip.dev/auth/4111
295+
# Attempt to use self-signed JWTs when a service account is used.
296+
# A default host must be explicitly provided.
297+
if (
298+
isinstance(self.credentials, service_account.Credentials)
299+
and self._default_host
300+
):
301+
self.credentials._create_self_signed_jwt(
302+
"https://{}/".format(self._default_host)
303+
)
304+
288305
super(AuthorizedHttp, self).__init__()
289306

290307
def configure_mtls_channel(self, client_cert_callback=None):

system_tests/noxfile.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,30 @@ def grpc(session):
293293
session.run("pytest", "system_tests_sync/test_grpc.py")
294294

295295

296+
@nox.session(python=PYTHON_VERSIONS_SYNC)
297+
def requests(session):
298+
session.install(LIBRARY_DIR)
299+
session.install(*TEST_DEPENDENCIES_SYNC)
300+
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
301+
session.run("pytest", "system_tests_sync/test_requests.py")
302+
303+
304+
@nox.session(python=PYTHON_VERSIONS_SYNC)
305+
def urllib3(session):
306+
session.install(LIBRARY_DIR)
307+
session.install(*TEST_DEPENDENCIES_SYNC)
308+
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
309+
session.run("pytest", "system_tests_sync/test_urllib3.py")
310+
311+
296312
@nox.session(python=PYTHON_VERSIONS_SYNC)
297313
def mtls_http(session):
298314
session.install(LIBRARY_DIR)
299315
session.install(*TEST_DEPENDENCIES_SYNC, "pyopenssl")
300316
session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
301317
session.run("pytest", "system_tests_sync/test_mtls_http.py")
302318

319+
303320
#ASYNC SYSTEM TESTS
304321

305322
@nox.session(python=PYTHON_VERSIONS_ASYNC)

system_tests/system_tests_sync/test_grpc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ def test_grpc_request_with_regular_credentials_and_self_signed_jwt(http_request)
5757
list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
5858
list(list_topics_iter)
5959

60-
# Check that self-signed JWT was created
60+
# Check that self-signed JWT was created and is being used
6161
assert credentials._jwt_credentials is not None
62+
assert credentials._jwt_credentials.token == credentials.token
6263

6364

6465
def test_grpc_request_with_jwt_credentials():
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import google.auth
16+
import google.auth.credentials
17+
import google.auth.transport.requests
18+
from google.oauth2 import service_account
19+
20+
21+
def test_authorized_session_with_service_account_and_self_signed_jwt():
22+
credentials, project_id = google.auth.default()
23+
24+
credentials = credentials.with_scopes(
25+
scopes=[],
26+
default_scopes=["https://www.googleapis.com/auth/pubsub"],
27+
)
28+
29+
session = google.auth.transport.requests.AuthorizedSession(
30+
credentials=credentials, default_host="pubsub.googleapis.com"
31+
)
32+
33+
# List Pub/Sub Topics through the REST API
34+
# https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
35+
response = session.get("https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id))
36+
response.raise_for_status()
37+
38+
# Check that self-signed JWT was created and is being used
39+
assert credentials._jwt_credentials is not None
40+
assert credentials._jwt_credentials.token == credentials.token
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import google.auth
16+
import google.auth.credentials
17+
import google.auth.transport.requests
18+
from google.oauth2 import service_account
19+
20+
21+
def test_authorized_session_with_service_account_and_self_signed_jwt():
22+
credentials, project_id = google.auth.default()
23+
24+
credentials = credentials.with_scopes(
25+
scopes=[],
26+
default_scopes=["https://www.googleapis.com/auth/pubsub"],
27+
)
28+
29+
http = google.auth.transport.urllib3.AuthorizedHttp(
30+
credentials=credentials, default_host="pubsub.googleapis.com"
31+
)
32+
33+
# List Pub/Sub Topics through the REST API
34+
# https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
35+
response = http.urlopen(
36+
method="GET",
37+
url="https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id)
38+
)
39+
40+
assert response.status == 200
41+
42+
# Check that self-signed JWT was created and is being used
43+
assert credentials._jwt_credentials is not None
44+
assert credentials._jwt_credentials.token == credentials.token

tests/transport/test_requests.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import google.auth.credentials
3131
import google.auth.transport._mtls_helper
3232
import google.auth.transport.requests
33+
from google.oauth2 import service_account
3334
from tests.transport import compliance
3435

3536

@@ -372,6 +373,25 @@ def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time):
372373
"GET", self.TEST_URL, timeout=60, max_allowed_time=2.9
373374
)
374375

376+
def test_authorized_session_without_default_host(self):
377+
credentials = mock.create_autospec(service_account.Credentials)
378+
379+
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
380+
381+
authed_session.credentials._create_self_signed_jwt.assert_not_called()
382+
383+
def test_authorized_session_with_default_host(self):
384+
default_host = "pubsub.googleapis.com"
385+
credentials = mock.create_autospec(service_account.Credentials)
386+
387+
authed_session = google.auth.transport.requests.AuthorizedSession(
388+
credentials, default_host=default_host
389+
)
390+
391+
authed_session.credentials._create_self_signed_jwt.assert_called_once_with(
392+
"https://{}/".format(default_host)
393+
)
394+
375395
def test_configure_mtls_channel_with_callback(self):
376396
mock_callback = mock.Mock()
377397
mock_callback.return_value = (

tests/transport/test_urllib3.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import google.auth.credentials
2727
import google.auth.transport._mtls_helper
2828
import google.auth.transport.urllib3
29+
from google.oauth2 import service_account
2930
from tests.transport import compliance
3031

3132

@@ -158,6 +159,25 @@ def test_urlopen_refresh(self):
158159
("GET", self.TEST_URL, None, {"authorization": "token1"}, {}),
159160
]
160161

162+
def test_urlopen_no_default_host(self):
163+
credentials = mock.create_autospec(service_account.Credentials)
164+
165+
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
166+
167+
authed_http.credentials._create_self_signed_jwt.assert_not_called()
168+
169+
def test_urlopen_with_default_host(self):
170+
default_host = "pubsub.googleapis.com"
171+
credentials = mock.create_autospec(service_account.Credentials)
172+
173+
authed_http = google.auth.transport.urllib3.AuthorizedHttp(
174+
credentials, default_host=default_host
175+
)
176+
177+
authed_http.credentials._create_self_signed_jwt.assert_called_once_with(
178+
"https://{}/".format(default_host)
179+
)
180+
161181
def test_proxies(self):
162182
http = mock.create_autospec(urllib3.PoolManager)
163183
authed_http = google.auth.transport.urllib3.AuthorizedHttp(None, http=http)

0 commit comments

Comments
 (0)