From 8479ed7bece4fc82476bbe3c5c189be5854f6d1d Mon Sep 17 00:00:00 2001 From: gaoflow Date: Thu, 25 Jun 2026 15:13:38 +0200 Subject: [PATCH] fix: reject tokens missing 'aud' claim when audience is expected When jwt.decode() is called with an audience= argument, tokens that do not carry an 'aud' claim were silently accepted. This enables cross- service token reuse: a valid token issued without an audience claim could be replayed against any service that specifies an expected audience. The fix activates the commented-out guard in _validate_aud: if the caller provides a non-None audience and the token has no 'aud' claim, raise JWTClaimsError instead of returning. The existing require_aud=True option remains the way to enforce the claim when audience= is not given. Tests: rename test_aud_empty_claim to two explicit cases (no-audience passes, audience-given raises), and fix test_require to not pass an audience value for non-aud required-claim tests. Fixes #407 --- jose/jwt.py | 4 ++-- tests/test_jwt.py | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/jose/jwt.py b/jose/jwt.py index f47e4dd..e8900a4 100644 --- a/jose/jwt.py +++ b/jose/jwt.py @@ -352,8 +352,8 @@ def _validate_aud(claims, audience=None): """ if "aud" not in claims: - # if audience: - # raise JWTError('Audience claim expected, but not in claims') + if audience is not None: + raise JWTClaimsError("Token is missing the 'aud' claim") return audience_claims = claims["aud"] diff --git a/tests/test_jwt.py b/tests/test_jwt.py index f9d54cd..9244c2d 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -353,11 +353,17 @@ def test_aud_case_sensitive(self, key): with pytest.raises(JWTError): jwt.decode(token, key, audience="AUDIENCE") - def test_aud_empty_claim(self, claims, key): - aud = "audience" + def test_aud_missing_claim_no_audience(self, claims, key): + # Token without 'aud' and no audience expected: should pass + token = jwt.encode(claims, key) + jwt.decode(token, key) + def test_aud_missing_claim_with_audience(self, claims, key): + # Token without 'aud' but caller requires an audience: should be rejected + aud = "audience" token = jwt.encode(claims, key) - jwt.decode(token, key, audience=aud) + with pytest.raises(JWTError): + jwt.decode(token, key, audience=aud) def test_aud_not_string_or_list(self, key): aud = 1 @@ -522,12 +528,15 @@ def test_require(self, claims, key, claim, value): if callable(value): value = value() options = {"require_" + claim: True, "verify_" + claim: False} + # Only pass audience when the claim under test is actually "aud"; + # passing audience for other claims would wrongly trigger the missing-aud check. + audience = str(value) if claim == "aud" else None token = jwt.encode(claims, key) with pytest.raises(JWTError): - jwt.decode(token, key, options=options, audience=str(value)) + jwt.decode(token, key, options=options, audience=audience) new_claims = dict(claims) new_claims[claim] = value token = jwt.encode(new_claims, key) - jwt.decode(token, key, options=options, audience=str(value)) + jwt.decode(token, key, options=options, audience=audience)