From 0087e2445614e7e905dc6f07b9bb4b0333dd22ec Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 24 Jul 2026 02:10:06 +0000 Subject: [PATCH 1/4] fix(oauth2): avoid redundant JWKS network fetches Addresses a performance regression resulting in duplicate fetches of the JWKS endpoint by utilizing the already-fetched certs payload via PyJWKSet.from_dict instead of deferring to PyJWKClient. --- .../google-auth/google/oauth2/id_token.py | 18 ++++++++++++--- .../google-auth/tests/oauth2/test_id_token.py | 22 +++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index d21be1a06c84..861f1218a8a8 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -124,7 +124,7 @@ def verify_token( intended for. If None then the audience is not verified. certs_url (str): The URL that specifies the certificates to use to verify the token. This URL should return JSON in the format of - ``{'key id': 'x509 certificate'}`` or a certificate array according to + ``{'key id': 'x509 certificate'}`` or a JWK Set according to the JWK spec (see https://tools.ietf.org/html/rfc7517). clock_skew_in_seconds (int): The clock skew used for `iat` and `exp` validation. @@ -137,12 +137,24 @@ def verify_token( if "keys" in certs: try: import jwt as jwt_lib # type: ignore + from jwt.api_jwk import PyJWKSet except ImportError as caught_exc: # pragma: NO COVER raise ImportError( "The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format." ) from caught_exc - jwks_client = jwt_lib.PyJWKClient(certs_url) - signing_key = jwks_client.get_signing_key_from_jwt(id_token) + jwkset = PyJWKSet.from_dict(certs) + header = jwt_lib.get_unverified_header(id_token) + kid = header.get("kid") + + signing_key = None + for key in jwkset.keys: + if key.key_id == kid: + signing_key = key + break + + if signing_key is None: + raise ValueError("Token has an invalid kid") + return jwt_lib.decode( id_token, signing_key.key, diff --git a/packages/google-auth/tests/oauth2/test_id_token.py b/packages/google-auth/tests/oauth2/test_id_token.py index 68b74f11bc1d..12b0d1faf46e 100644 --- a/packages/google-auth/tests/oauth2/test_id_token.py +++ b/packages/google-auth/tests/oauth2/test_id_token.py @@ -86,24 +86,32 @@ def test_verify_token(_fetch_certs, decode): @mock.patch("google.oauth2.id_token._fetch_certs", autospec=True) -@mock.patch("jwt.PyJWKClient", autospec=True) +@mock.patch("jwt.api_jwk.PyJWKSet", autospec=True) +@mock.patch("jwt.get_unverified_header", autospec=True) @mock.patch("jwt.decode", autospec=True) -def test_verify_token_jwk(decode, py_jwk, _fetch_certs): +def test_verify_token_jwk(decode, get_unverified_header, py_jwk_set, _fetch_certs): certs_url = "abc123" data = {"keys": [{"alg": "RS256"}]} _fetch_certs.return_value = data + get_unverified_header.return_value = {"kid": "mock-kid"} + + mock_key = mock.Mock() + mock_key.key_id = "mock-kid" + mock_key.key = mock.sentinel.key + mock_key.algorithm_name = "mock-alg" + py_jwk_set.from_dict.return_value.keys = [mock_key] result = id_token.verify_token( mock.sentinel.token, mock.sentinel.request, certs_url=certs_url ) assert result == decode.return_value - py_jwk.assert_called_once_with(certs_url) - signing_key = py_jwk.return_value.get_signing_key_from_jwt + py_jwk_set.from_dict.assert_called_once_with(data) + get_unverified_header.assert_called_once_with(mock.sentinel.token) + _fetch_certs.assert_called_once_with(mock.sentinel.request, certs_url) - signing_key.assert_called_once_with(mock.sentinel.token) decode.assert_called_once_with( mock.sentinel.token, - signing_key.return_value.key, - algorithms=[signing_key.return_value.algorithm_name], + mock.sentinel.key, + algorithms=["mock-alg"], audience=None, ) From def353c5888629de0c2034e77244f08fe5a7898c Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 24 Jul 2026 02:25:48 +0000 Subject: [PATCH 2/4] test: suppress mypy error for internal jwt.api_jwk import --- packages/google-auth/google/oauth2/id_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index 861f1218a8a8..c5ac8662a9c6 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -137,7 +137,7 @@ def verify_token( if "keys" in certs: try: import jwt as jwt_lib # type: ignore - from jwt.api_jwk import PyJWKSet + from jwt.api_jwk import PyJWKSet # type: ignore except ImportError as caught_exc: # pragma: NO COVER raise ImportError( "The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format." From c793b1c0778614d8be9278f693b14f7c2bee0938 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 24 Jul 2026 02:29:43 +0000 Subject: [PATCH 3/4] fix(oauth2): cast id_token to string for mypy --- mypy.ini | 3 +++ packages/google-auth/google/oauth2/id_token.py | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index bf2a196c5ab7..19b239414608 100644 --- a/mypy.ini +++ b/mypy.ini @@ -57,6 +57,9 @@ ignore_missing_imports = True [mypy-grpc_status] ignore_missing_imports = True +[mypy-jwt,jwt.*] +ignore_missing_imports = True + [mypy-ibis.*] ignore_missing_imports = True diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index c5ac8662a9c6..173460470f3e 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -132,12 +132,15 @@ def verify_token( Returns: Mapping[str, Any]: The decoded token. """ + if isinstance(id_token, bytes): + id_token = id_token.decode("utf-8") + certs = _fetch_certs(request, certs_url) if "keys" in certs: try: - import jwt as jwt_lib # type: ignore - from jwt.api_jwk import PyJWKSet # type: ignore + import jwt as jwt_lib + from jwt.api_jwk import PyJWKSet except ImportError as caught_exc: # pragma: NO COVER raise ImportError( "The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format." From 2f65410f05126fbe9f09a70add4a8281163ab2cb Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 24 Jul 2026 20:28:05 +0000 Subject: [PATCH 4/4] fix(oauth2): enforce sig usage on JWK set --- packages/google-auth/google/oauth2/id_token.py | 2 +- packages/google-auth/tests/oauth2/test_id_token.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index 173460470f3e..c65fcfddbc66 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -151,7 +151,7 @@ def verify_token( signing_key = None for key in jwkset.keys: - if key.key_id == kid: + if kid and key.key_id == kid and key.public_key_use in ("sig", None): signing_key = key break diff --git a/packages/google-auth/tests/oauth2/test_id_token.py b/packages/google-auth/tests/oauth2/test_id_token.py index 12b0d1faf46e..b869da058b08 100644 --- a/packages/google-auth/tests/oauth2/test_id_token.py +++ b/packages/google-auth/tests/oauth2/test_id_token.py @@ -95,8 +95,9 @@ def test_verify_token_jwk(decode, get_unverified_header, py_jwk_set, _fetch_cert _fetch_certs.return_value = data get_unverified_header.return_value = {"kid": "mock-kid"} - mock_key = mock.Mock() + mock_key = mock.MagicMock() mock_key.key_id = "mock-kid" + mock_key.public_key_use = "sig" mock_key.key = mock.sentinel.key mock_key.algorithm_name = "mock-alg" py_jwk_set.from_dict.return_value.keys = [mock_key]