Skip to content

Commit 98c3ed9

Browse files
feat: allow users to use jwk keys for verifying ID token (#1641)
* add support for jwk format * update formatting * formatting changes * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * remove pyjwt --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 1592e39 commit 98c3ed9

File tree

7 files changed

+56
-12
lines changed

7 files changed

+56
-12
lines changed

docs/requirements-docs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ cryptography
22
sphinx-docstring-typing
33
urllib3
44
requests
5-
requests-oauthlib
5+
requests-oauthlib

google/oauth2/id_token.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,17 @@ def _fetch_certs(request, certs_url):
8282
"""Fetches certificates.
8383
8484
Google-style cerificate endpoints return JSON in the format of
85-
``{'key id': 'x509 certificate'}``.
85+
``{'key id': 'x509 certificate'}`` or a certificate array according
86+
to the JWK spec (see https://tools.ietf.org/html/rfc7517).
8687
8788
Args:
8889
request (google.auth.transport.Request): The object used to make
8990
HTTP requests.
9091
certs_url (str): The certificate endpoint URL.
9192
9293
Returns:
93-
Mapping[str, str]: A mapping of public key ID to x.509 certificate
94-
data.
94+
Mapping[str, str] | Mapping[str, list]: A mapping of public keys
95+
in x.509 or JWK spec.
9596
"""
9697
response = request(certs_url, method="GET")
9798

@@ -120,7 +121,8 @@ def verify_token(
120121
intended for. If None then the audience is not verified.
121122
certs_url (str): The URL that specifies the certificates to use to
122123
verify the token. This URL should return JSON in the format of
123-
``{'key id': 'x509 certificate'}``.
124+
``{'key id': 'x509 certificate'}`` or a certificate array according to
125+
the JWK spec (see https://tools.ietf.org/html/rfc7517).
124126
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
125127
validation.
126128
@@ -129,12 +131,28 @@ def verify_token(
129131
"""
130132
certs = _fetch_certs(request, certs_url)
131133

132-
return jwt.decode(
133-
id_token,
134-
certs=certs,
135-
audience=audience,
136-
clock_skew_in_seconds=clock_skew_in_seconds,
137-
)
134+
if "keys" in certs:
135+
try:
136+
import jwt as jwt_lib # type: ignore
137+
except ImportError as caught_exc: # pragma: NO COVER
138+
raise ImportError(
139+
"The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format."
140+
) from caught_exc
141+
jwks_client = jwt_lib.PyJWKClient(certs_url)
142+
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
143+
return jwt_lib.decode(
144+
id_token,
145+
signing_key.key,
146+
algorithms=[signing_key.algorithm_name],
147+
audience=audience,
148+
)
149+
else:
150+
return jwt.decode(
151+
id_token,
152+
certs=certs,
153+
audience=audience,
154+
clock_skew_in_seconds=clock_skew_in_seconds,
155+
)
138156

139157

140158
def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"requests": "requests >= 2.20.0, < 3.0.0.dev0",
3434
"reauth": "pyu2f>=0.1.5",
3535
"enterprise_cert": ["cryptography", "pyopenssl"],
36+
"pyjwt": ["pyjwt>=2.0", "cryptography>=38.0.3"],
3637
}
3738

3839
with io.open("README.rst", "r") as fh:

system_tests/noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def configure_cloud_sdk(session, application_default_credentials, project=False)
162162
# Test sesssions
163163

164164
TEST_DEPENDENCIES_ASYNC = ["aiohttp", "pytest-asyncio", "nest-asyncio", "mock"]
165-
TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock"]
165+
TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock", "pyjwt"]
166166
PYTHON_VERSIONS_ASYNC = ["3.7"]
167167
PYTHON_VERSIONS_SYNC = ["3.7"]
168168

testing/constraints-3.7.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ setuptools==40.3.0
1111
rsa==3.1.4
1212
aiohttp==3.6.2
1313
requests==2.20.0
14+
pyjwt==2.0

testing/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pytest
88
pytest-cov
99
pytest-localserver
1010
pyu2f
11+
pyjwt
1112
requests
1213
urllib3
1314
cryptography < 39.0.0

tests/oauth2/test_id_token.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ def test_verify_token(_fetch_certs, decode):
7878
)
7979

8080

81+
@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
82+
@mock.patch("jwt.PyJWKClient", autospec=True)
83+
@mock.patch("jwt.decode", autospec=True)
84+
def test_verify_token_jwk(decode, py_jwk, _fetch_certs):
85+
certs_url = "abc123"
86+
data = {"keys": [{"alg": "RS256"}]}
87+
_fetch_certs.return_value = data
88+
result = id_token.verify_token(
89+
mock.sentinel.token, mock.sentinel.request, certs_url=certs_url
90+
)
91+
assert result == decode.return_value
92+
py_jwk.assert_called_once_with(certs_url)
93+
signing_key = py_jwk.return_value.get_signing_key_from_jwt
94+
_fetch_certs.assert_called_once_with(mock.sentinel.request, certs_url)
95+
signing_key.assert_called_once_with(mock.sentinel.token)
96+
decode.assert_called_once_with(
97+
mock.sentinel.token,
98+
signing_key.return_value.key,
99+
algorithms=[signing_key.return_value.algorithm_name],
100+
audience=None,
101+
)
102+
103+
81104
@mock.patch("google.auth.jwt.decode", autospec=True)
82105
@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
83106
def test_verify_token_args(_fetch_certs, decode):

0 commit comments

Comments
 (0)