Skip to content

Commit 1b7f57a

Browse files
committed
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.
1 parent dd30194 commit 1b7f57a

2 files changed

Lines changed: 30 additions & 10 deletions

File tree

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def verify_token(
124124
intended for. If None then the audience is not verified.
125125
certs_url (str): The URL that specifies the certificates to use to
126126
verify the token. This URL should return JSON in the format of
127-
``{'key id': 'x509 certificate'}`` or a certificate array according to
127+
``{'key id': 'x509 certificate'}`` or a JWK Set according to
128128
the JWK spec (see https://tools.ietf.org/html/rfc7517).
129129
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
130130
validation.
@@ -137,12 +137,24 @@ def verify_token(
137137
if "keys" in certs:
138138
try:
139139
import jwt as jwt_lib # type: ignore
140+
from jwt.api_jwk import PyJWKSet
140141
except ImportError as caught_exc: # pragma: NO COVER
141142
raise ImportError(
142143
"The pyjwt library is not installed, please install the pyjwt package to use the jwk certs format."
143144
) from caught_exc
144-
jwks_client = jwt_lib.PyJWKClient(certs_url)
145-
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
145+
jwkset = PyJWKSet.from_dict(certs)
146+
header = jwt_lib.get_unverified_header(id_token)
147+
kid = header.get("kid")
148+
149+
signing_key = None
150+
for key in jwkset.keys:
151+
if key.key_id == kid:
152+
signing_key = key
153+
break
154+
155+
if signing_key is None:
156+
raise ValueError("Token has an invalid kid")
157+
146158
return jwt_lib.decode(
147159
id_token,
148160
signing_key.key,

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,32 @@ def test_verify_token(_fetch_certs, decode):
8686

8787

8888
@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
89-
@mock.patch("jwt.PyJWKClient", autospec=True)
89+
@mock.patch("jwt.api_jwk.PyJWKSet", autospec=True)
90+
@mock.patch("jwt.get_unverified_header", autospec=True)
9091
@mock.patch("jwt.decode", autospec=True)
91-
def test_verify_token_jwk(decode, py_jwk, _fetch_certs):
92+
def test_verify_token_jwk(decode, get_unverified_header, py_jwk_set, _fetch_certs):
9293
certs_url = "abc123"
9394
data = {"keys": [{"alg": "RS256"}]}
9495
_fetch_certs.return_value = data
96+
get_unverified_header.return_value = {"kid": "mock-kid"}
97+
98+
mock_key = mock.Mock()
99+
mock_key.key_id = "mock-kid"
100+
mock_key.key = mock.sentinel.key
101+
mock_key.algorithm_name = "mock-alg"
102+
py_jwk_set.from_dict.return_value.keys = [mock_key]
95103
result = id_token.verify_token(
96104
mock.sentinel.token, mock.sentinel.request, certs_url=certs_url
97105
)
98106
assert result == decode.return_value
99-
py_jwk.assert_called_once_with(certs_url)
100-
signing_key = py_jwk.return_value.get_signing_key_from_jwt
107+
py_jwk_set.from_dict.assert_called_once_with(data)
108+
get_unverified_header.assert_called_once_with(mock.sentinel.token)
109+
101110
_fetch_certs.assert_called_once_with(mock.sentinel.request, certs_url)
102-
signing_key.assert_called_once_with(mock.sentinel.token)
103111
decode.assert_called_once_with(
104112
mock.sentinel.token,
105-
signing_key.return_value.key,
106-
algorithms=[signing_key.return_value.algorithm_name],
113+
mock.sentinel.key,
114+
algorithms=["mock-alg"],
107115
audience=None,
108116
)
109117

0 commit comments

Comments
 (0)