Skip to content

Commit 36f686e

Browse files
feat: allow scopes for self signed jwt (googleapis#776)
* feat: allow scopes for self signed jwt * Update service_account.py * add http changes * Update google/auth/jwt.py
1 parent 4057e0a commit 36f686e

10 files changed

Lines changed: 136 additions & 24 deletions

File tree

packages/google-auth/google/auth/jwt.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,9 @@ def _make_jwt(self):
525525
"sub": self._subject,
526526
"iat": _helpers.datetime_to_secs(now),
527527
"exp": _helpers.datetime_to_secs(expiry),
528-
"aud": self._audience,
529528
}
529+
if self._audience:
530+
payload["aud"] = self._audience
530531

531532
payload.update(self._additional_claims)
532533

packages/google-auth/google/auth/transport/grpc.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,9 @@ def _get_authorization_headers(self, context):
7979
# Attempt to use self-signed JWTs when a service account is used.
8080
# A default host must be explicitly provided since it cannot always
8181
# be determined from the context.service_url.
82-
if (
83-
isinstance(self._credentials, service_account.Credentials)
84-
and self._default_host
85-
):
82+
if isinstance(self._credentials, service_account.Credentials):
8683
self._credentials._create_self_signed_jwt(
87-
"https://{}/".format(self._default_host)
84+
"https://{}/".format(self._default_host) if self._default_host else None
8885
)
8986

9087
self._credentials.before_request(

packages/google-auth/google/auth/transport/requests.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,9 @@ def __init__(
358358

359359
# https://google.aip.dev/auth/4111
360360
# Attempt to use self-signed JWTs when a service account is used.
361-
# A default host must be explicitly provided.
362-
if (
363-
isinstance(self.credentials, service_account.Credentials)
364-
and self._default_host
365-
):
361+
if isinstance(self.credentials, service_account.Credentials):
366362
self.credentials._create_self_signed_jwt(
367-
"https://{}/".format(self._default_host)
363+
"https://{}/".format(self._default_host) if self._default_host else None
368364
)
369365

370366
def configure_mtls_channel(self, client_cert_callback=None):

packages/google-auth/google/auth/transport/urllib3.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,9 @@ def __init__(
293293

294294
# https://google.aip.dev/auth/4111
295295
# 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-
):
296+
if isinstance(self.credentials, service_account.Credentials):
301297
self.credentials._create_self_signed_jwt(
302-
"https://{}/".format(self._default_host)
298+
"https://{}/".format(self._default_host) if self._default_host else None
303299
)
304300

305301
super(AuthorizedHttp, self).__init__()

packages/google-auth/google/oauth2/service_account.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def __init__(
131131
project_id=None,
132132
quota_project_id=None,
133133
additional_claims=None,
134+
always_use_jwt_access=False,
134135
):
135136
"""
136137
Args:
@@ -149,6 +150,8 @@ def __init__(
149150
billing.
150151
additional_claims (Mapping[str, str]): Any additional claims for
151152
the JWT assertion used in the authorization grant.
153+
always_use_jwt_access (Optional[bool]): Whether self signed JWT should
154+
be always used.
152155
153156
.. note:: Typically one of the helper constructors
154157
:meth:`from_service_account_file` or
@@ -165,6 +168,7 @@ def __init__(
165168
self._project_id = project_id
166169
self._quota_project_id = quota_project_id
167170
self._token_uri = token_uri
171+
self._always_use_jwt_access = always_use_jwt_access
168172

169173
self._jwt_credentials = None
170174

@@ -266,6 +270,30 @@ def with_scopes(self, scopes, default_scopes=None):
266270
project_id=self._project_id,
267271
quota_project_id=self._quota_project_id,
268272
additional_claims=self._additional_claims.copy(),
273+
always_use_jwt_access=self._always_use_jwt_access,
274+
)
275+
276+
def with_always_use_jwt_access(self, always_use_jwt_access):
277+
"""Create a copy of these credentials with the specified always_use_jwt_access value.
278+
279+
Args:
280+
always_use_jwt_access (bool): Whether always use self signed JWT or not.
281+
282+
Returns:
283+
google.auth.service_account.Credentials: A new credentials
284+
instance.
285+
"""
286+
return self.__class__(
287+
self._signer,
288+
service_account_email=self._service_account_email,
289+
scopes=self._scopes,
290+
default_scopes=self._default_scopes,
291+
token_uri=self._token_uri,
292+
subject=self._subject,
293+
project_id=self._project_id,
294+
quota_project_id=self._quota_project_id,
295+
additional_claims=self._additional_claims.copy(),
296+
always_use_jwt_access=always_use_jwt_access,
269297
)
270298

271299
def with_subject(self, subject):
@@ -288,6 +316,7 @@ def with_subject(self, subject):
288316
project_id=self._project_id,
289317
quota_project_id=self._quota_project_id,
290318
additional_claims=self._additional_claims.copy(),
319+
always_use_jwt_access=self._always_use_jwt_access,
291320
)
292321

293322
def with_claims(self, additional_claims):
@@ -315,6 +344,7 @@ def with_claims(self, additional_claims):
315344
project_id=self._project_id,
316345
quota_project_id=self._quota_project_id,
317346
additional_claims=new_additional_claims,
347+
always_use_jwt_access=self._always_use_jwt_access,
318348
)
319349

320350
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
@@ -330,6 +360,7 @@ def with_quota_project(self, quota_project_id):
330360
project_id=self._project_id,
331361
quota_project_id=quota_project_id,
332362
additional_claims=self._additional_claims.copy(),
363+
always_use_jwt_access=self._always_use_jwt_access,
333364
)
334365

335366
def _make_authorization_grant_assertion(self):
@@ -386,8 +417,22 @@ def _create_self_signed_jwt(self, audience):
386417
audience (str): The service URL. ``https://[API_ENDPOINT]/``
387418
"""
388419
# https://google.aip.dev/auth/4111
389-
# If the user has not defined scopes, create a self-signed jwt
390-
if not self.scopes:
420+
if self._always_use_jwt_access:
421+
if self._scopes:
422+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
423+
self, None, additional_claims={"scope": " ".join(self._scopes)}
424+
)
425+
elif audience:
426+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
427+
self, audience
428+
)
429+
elif self._default_scopes:
430+
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
431+
self,
432+
None,
433+
additional_claims={"scope": " ".join(self._default_scopes)},
434+
)
435+
elif not self._scopes and audience:
391436
self._jwt_credentials = jwt.Credentials.from_signing_credentials(
392437
self, audience
393438
)

packages/google-auth/tests/oauth2/test_service_account.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ def test_with_quota_project(self):
155155
new_credentials.apply(hdrs, token="tok")
156156
assert "x-goog-user-project" in hdrs
157157

158+
def test__with_always_use_jwt_access(self):
159+
credentials = self.make_credentials()
160+
assert not credentials._always_use_jwt_access
161+
162+
new_credentials = credentials.with_always_use_jwt_access(True)
163+
assert new_credentials._always_use_jwt_access
164+
158165
def test__make_authorization_grant_assertion(self):
159166
credentials = self.make_credentials()
160167
token = credentials._make_authorization_grant_assertion()
@@ -225,6 +232,65 @@ def test__create_self_signed_jwt_with_user_scopes(self, jwt):
225232
# JWT should not be created if there are user-defined scopes
226233
jwt.from_signing_credentials.assert_not_called()
227234

235+
@mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
236+
def test__create_self_signed_jwt_always_use_jwt_access_with_audience(self, jwt):
237+
credentials = service_account.Credentials(
238+
SIGNER,
239+
self.SERVICE_ACCOUNT_EMAIL,
240+
self.TOKEN_URI,
241+
default_scopes=["bar", "foo"],
242+
always_use_jwt_access=True,
243+
)
244+
245+
audience = "https://pubsub.googleapis.com"
246+
credentials._create_self_signed_jwt(audience)
247+
jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
248+
249+
@mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
250+
def test__create_self_signed_jwt_always_use_jwt_access_with_scopes(self, jwt):
251+
credentials = service_account.Credentials(
252+
SIGNER,
253+
self.SERVICE_ACCOUNT_EMAIL,
254+
self.TOKEN_URI,
255+
scopes=["bar", "foo"],
256+
always_use_jwt_access=True,
257+
)
258+
259+
audience = "https://pubsub.googleapis.com"
260+
credentials._create_self_signed_jwt(audience)
261+
jwt.from_signing_credentials.assert_called_once_with(
262+
credentials, None, additional_claims={"scope": "bar foo"}
263+
)
264+
265+
@mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
266+
def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes(
267+
self, jwt
268+
):
269+
credentials = service_account.Credentials(
270+
SIGNER,
271+
self.SERVICE_ACCOUNT_EMAIL,
272+
self.TOKEN_URI,
273+
default_scopes=["bar", "foo"],
274+
always_use_jwt_access=True,
275+
)
276+
277+
credentials._create_self_signed_jwt(None)
278+
jwt.from_signing_credentials.assert_called_once_with(
279+
credentials, None, additional_claims={"scope": "bar foo"}
280+
)
281+
282+
@mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
283+
def test__create_self_signed_jwt_always_use_jwt_access(self, jwt):
284+
credentials = service_account.Credentials(
285+
SIGNER,
286+
self.SERVICE_ACCOUNT_EMAIL,
287+
self.TOKEN_URI,
288+
always_use_jwt_access=True,
289+
)
290+
291+
credentials._create_self_signed_jwt(None)
292+
jwt.from_signing_credentials.assert_not_called()
293+
228294
@mock.patch("google.oauth2._client.jwt_grant", autospec=True)
229295
def test_refresh_success(self, jwt_grant):
230296
credentials = self.make_credentials()

packages/google-auth/tests/test_jwt.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,18 @@ def test_with_claims(self):
390390
assert new_credentials._additional_claims == self.credentials._additional_claims
391391
assert new_credentials._quota_project_id == self.credentials._quota_project_id
392392

393+
def test__make_jwt_without_audience(self):
394+
cred = jwt.Credentials.from_service_account_info(
395+
SERVICE_ACCOUNT_INFO.copy(),
396+
subject=self.SUBJECT,
397+
audience=None,
398+
additional_claims={"scope": "foo bar"},
399+
)
400+
token, _ = cred._make_jwt()
401+
payload = jwt.decode(token, PUBLIC_CERT_BYTES)
402+
assert payload["scope"] == "foo bar"
403+
assert "aud" not in payload
404+
393405
def test_with_quota_project(self):
394406
quota_project_id = "project-foo"
395407

packages/google-auth/tests/transport/test_grpc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ def test__get_authorization_headers_with_service_account(self):
111111

112112
plugin._get_authorization_headers(context)
113113

114-
# self-signed JWT should not be created when default_host is not set
115-
credentials._create_self_signed_jwt.assert_not_called()
114+
credentials._create_self_signed_jwt.assert_called_once_with(None)
116115

117116
def test__get_authorization_headers_with_service_account_and_default_host(self):
118117
credentials = mock.create_autospec(service_account.Credentials)

packages/google-auth/tests/transport/test_requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def test_authorized_session_without_default_host(self):
378378

379379
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
380380

381-
authed_session.credentials._create_self_signed_jwt.assert_not_called()
381+
authed_session.credentials._create_self_signed_jwt.assert_called_once_with(None)
382382

383383
def test_authorized_session_with_default_host(self):
384384
default_host = "pubsub.googleapis.com"

packages/google-auth/tests/transport/test_urllib3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def test_urlopen_no_default_host(self):
164164

165165
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
166166

167-
authed_http.credentials._create_self_signed_jwt.assert_not_called()
167+
authed_http.credentials._create_self_signed_jwt.assert_called_once_with(None)
168168

169169
def test_urlopen_with_default_host(self):
170170
default_host = "pubsub.googleapis.com"

0 commit comments

Comments
 (0)