Repository navigation
fix(auth): enforce exp/iat claims and propagate leeway in verify_token JWKS branch - #19050
phamtoanbaoquan23121985 wants to merge 2 commits into
Conversation
…n JWKS branch
In `google.oauth2.id_token.verify_token`, when certificates are delivered
in JSON Web Key Set (JWKS) format (containing `"keys"`), verification is
delegated to `pyjwt`'s `jwt_lib.decode()`.
Prior to this fix:
1. `options={"require": ["exp", "iat"]}` was omitted, causing tokens lacking
an expiration claim (`exp`) or issued-at claim (`iat`) to be accepted as
valid without lifetime boundaries. In contrast, the certificate-mapping (PEM)
branch strictly enforces `exp` and `iat` presence via `_verify_iat_and_exp`.
2. `leeway=clock_skew_in_seconds` was omitted, causing user-configured clock
skew tolerance to be silently discarded in the JWKS branch, rejecting tokens
that had acceptable time drift within the configured skew window.
This change aligns JWKS verification semantics with the certificate-mapping
branch and OpenID Connect Core 1.0 (Section 3.1.3.7) by:
- Requiring both `exp` and `iat` claims in the PyJWT options dictionary.
- Propagating `leeway=clock_skew_in_seconds` to `jwt_lib.decode()`.
- Adding unit test verification for leeway propagation and require claims.
- Adding comprehensive differential tests verifying JWKS and PEM parity.
There was a problem hiding this comment.
Code Review
This pull request updates the JWK token verification path in google/oauth2/id_token.py to correctly pass leeway (configured via clock_skew_in_seconds) and validation options to jwt.decode. It also introduces comprehensive unit and differential tests to verify behavior under various token conditions. The review feedback suggests increasing the time offsets and leeway values in the clock skew tests from seconds to minutes to prevent test flakiness and loss of coverage in slow CI environments.
| def test_differential_clock_skew_expired_within_leeway(crypto_env): | ||
| now = int(time.time()) | ||
| payload = { | ||
| "iss": "https://accounts.google.com", | ||
| "sub": "user-123", | ||
| "aud": "my-client-app", | ||
| "iat": now - 100, | ||
| "exp": now - 5, # expired 5 seconds ago | ||
| } | ||
| token = _make_token(crypto_env, payload) | ||
|
|
||
| # With 10s leeway, both must accept | ||
| crypto_env["server"].mode = "jwks" | ||
| info_jwks = id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
| assert info_jwks["sub"] == "user-123" | ||
|
|
||
| crypto_env["server"].mode = "pem" | ||
| info_pem = id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
| assert info_pem["sub"] == "user-123" |
There was a problem hiding this comment.
Using small time offsets (e.g., 5 seconds) with real system time (time.time()) can lead to test flakiness in slow CI environments. If the test runner takes more than 5 seconds to execute between generating the token and verifying it, the token will be considered fully expired even with the 10-second leeway, causing the test to fail. Increasing the time offsets and leeway (e.g., to 5 and 10 minutes) provides a robust buffer.
def test_differential_clock_skew_expired_within_leeway(crypto_env):
now = int(time.time())
payload = {
"iss": "https://accounts.google.com",
"sub": "user-123",
"aud": "my-client-app",
"iat": now - 1000,
"exp": now - 300, # expired 5 minutes ago
}
token = _make_token(crypto_env, payload)
# With 10 minutes leeway, both must accept
crypto_env["server"].mode = "jwks"
info_jwks = id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=600,
)
assert info_jwks["sub"] == "user-123"
crypto_env["server"].mode = "pem"
info_pem = id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=600,
)
assert info_pem["sub"] == "user-123"| def test_differential_clock_skew_expired_outside_leeway(crypto_env): | ||
| now = int(time.time()) | ||
| payload = { | ||
| "iss": "https://accounts.google.com", | ||
| "sub": "user-123", | ||
| "aud": "my-client-app", | ||
| "iat": now - 100, | ||
| "exp": now - 20, # expired 20 seconds ago | ||
| } | ||
| token = _make_token(crypto_env, payload) | ||
|
|
||
| # With 10s leeway, both must reject | ||
| crypto_env["server"].mode = "jwks" | ||
| with pytest.raises(pyjwt.ExpiredSignatureError): | ||
| id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
|
|
||
| crypto_env["server"].mode = "pem" | ||
| with pytest.raises(exceptions.InvalidValue): | ||
| id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
|
|
There was a problem hiding this comment.
To maintain consistency with the other clock skew tests and ensure robustness against slow CI environments, increase the expiration offset and leeway values (e.g., to 10 minutes expired and 5 minutes leeway).
def test_differential_clock_skew_expired_outside_leeway(crypto_env):
now = int(time.time())
payload = {
"iss": "https://accounts.google.com",
"sub": "user-123",
"aud": "my-client-app",
"iat": now - 1000,
"exp": now - 600, # expired 10 minutes ago
}
token = _make_token(crypto_env, payload)
# With 5 minutes leeway, both must reject
crypto_env["server"].mode = "jwks"
with pytest.raises(pyjwt.ExpiredSignatureError):
id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=300,
)
crypto_env["server"].mode = "pem"
with pytest.raises(exceptions.InvalidValue):
id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=300,
)| def test_differential_clock_skew_future_iat_within_leeway(crypto_env): | ||
| now = int(time.time()) | ||
| payload = { | ||
| "iss": "https://accounts.google.com", | ||
| "sub": "user-123", | ||
| "aud": "my-client-app", | ||
| "iat": now + 5, # issued 5 seconds in future | ||
| "exp": now + 3600, | ||
| } | ||
| token = _make_token(crypto_env, payload) | ||
|
|
||
| # With 10s leeway, both must accept | ||
| crypto_env["server"].mode = "jwks" | ||
| info_jwks = id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
| assert info_jwks["sub"] == "user-123" | ||
|
|
||
| crypto_env["server"].mode = "pem" | ||
| info_pem = id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
| assert info_pem["sub"] == "user-123" |
There was a problem hiding this comment.
Using small time offsets (e.g., 5 seconds) with real system time (time.time()) can lead to loss of test coverage in slow CI environments. If the test runner takes more than 5 seconds to execute, the iat claim will no longer be in the future, meaning the test will no longer actually verify the future iat leeway handling. Increasing the time offsets and leeway (e.g., to 5 and 10 minutes) ensures the test remains valid.
def test_differential_clock_skew_future_iat_within_leeway(crypto_env):
now = int(time.time())
payload = {
"iss": "https://accounts.google.com",
"sub": "user-123",
"aud": "my-client-app",
"iat": now + 300, # issued 5 minutes in future
"exp": now + 3600,
}
token = _make_token(crypto_env, payload)
# With 10 minutes leeway, both must accept
crypto_env["server"].mode = "jwks"
info_jwks = id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=600,
)
assert info_jwks["sub"] == "user-123"
crypto_env["server"].mode = "pem"
info_pem = id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=600,
)
assert info_pem["sub"] == "user-123"| def test_differential_clock_skew_future_iat_outside_leeway(crypto_env): | ||
| now = int(time.time()) | ||
| payload = { | ||
| "iss": "https://accounts.google.com", | ||
| "sub": "user-123", | ||
| "aud": "my-client-app", | ||
| "iat": now + 20, # issued 20 seconds in future | ||
| "exp": now + 3600, | ||
| } | ||
| token = _make_token(crypto_env, payload) | ||
|
|
||
| # With 10s leeway, both must reject | ||
| crypto_env["server"].mode = "jwks" | ||
| with pytest.raises(pyjwt.ImmatureSignatureError): | ||
| id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
|
|
||
| crypto_env["server"].mode = "pem" | ||
| with pytest.raises(exceptions.InvalidValue): | ||
| id_token.verify_token( | ||
| token, | ||
| crypto_env["req"], | ||
| audience="my-client-app", | ||
| certs_url=crypto_env["server"].url, | ||
| clock_skew_in_seconds=10, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Using small time offsets (e.g., 20 seconds) with real system time (time.time()) can lead to test flakiness in slow CI environments. If the test runner takes more than 10 seconds to execute, the remaining future time will fall within the 10-second leeway, causing the token to be accepted and the test to fail. Increasing the time offsets and leeway (e.g., to 10 and 5 minutes) ensures the test remains robust.
def test_differential_clock_skew_future_iat_outside_leeway(crypto_env):
now = int(time.time())
payload = {
"iss": "https://accounts.google.com",
"sub": "user-123",
"aud": "my-client-app",
"iat": now + 600, # issued 10 minutes in future
"exp": now + 3600,
}
token = _make_token(crypto_env, payload)
# With 5 minutes leeway, both must reject
crypto_env["server"].mode = "jwks"
with pytest.raises(pyjwt.ImmatureSignatureError):
id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=300,
)
crypto_env["server"].mode = "pem"
with pytest.raises(exceptions.InvalidValue):
id_token.verify_token(
token,
crypto_env["req"],
audience="my-client-app",
certs_url=crypto_env["server"].url,
clock_skew_in_seconds=300,
)
Pull Request: fix(auth): enforce exp/iat claims and propagate leeway in verify_token JWKS branch
Target Branch:
googleapis/google-cloud-python:mainSource Branch:
fix/auth-id-token-jwks-claims-leewayPackage:
packages/google-authDescription
In
google.oauth2.id_token.verify_token, when certificate keys are provided in JSON Web Key Set (JWKS) format (i.e., JSON payload containing a"keys"array), verification delegates decoding to PyJWT'sjwt_lib.decode().Prior to this PR, two discrepancies existed between the JWKS branch and the certificate-mapping (PEM) branch:
options={"require": ["exp", "iat"]}was omitted in the call tojwt_lib.decode(). Consequently, tokens lacking an expiration (exp) or issued-at (iat) claim were decoded and returned without error. Under the PEM branch (google.auth.jwt.decode), missingexporiatclaims immediately raiseMalformedErrorvia_verify_iat_and_exp.clock_skew_in_secondswas not passed tojwt_lib.decode(..., leeway=clock_skew_in_seconds). This caused user-configured clock skew tolerances to be silently dropped in the JWKS branch, rejecting tokens that had minor, legitimate clock drift within the configured tolerance window.This pull request resolves both discrepancies by:
leeway=clock_skew_in_seconds.options={"verify_signature": True, "verify_exp": True, "require": ["exp", "iat"]}.leewayandrequireoptions.Specifications & References
Testing & Verification
All 90 unit, async, and differential tests pass with exit code
0:packages/google-auth/tests/oauth2/test_id_token.pypackages/google-auth/tests/oauth2/test_id_token_differential.pypackages/google-auth/tests_async/oauth2/test_id_token.pyDifferential Cryptographic Matrix (11 Scenarios)
exp,iat)expclaimiatclaimiatwithin leeway (5s future, 10s skew)iatoutside leeway (25s future, 10s skew)Checklist
google-cloud-python.git diff --checkwith 0 whitespace errors.