From 232d64736b5bae9ff06f378854b11fafbf773b5d Mon Sep 17 00:00:00 2001 From: choksi2212 Date: Fri, 23 Oct 2026 21:16:56 +0530 Subject: [PATCH] fix: treat claims option "value" as the expected claim value A list passed to the "value" option was wrapped into a nested list before the membership check, so a claim equal to the configured value was rejected, e.g.:: JWTClaimsRegistry(aud={"essential": True, "value": ["id1"]}).validate({"aud": ["id1"]}) Compare the claim with the configured value itself: a list claim matches when it equals the configured value or contains it, and a scalar claim still has to equal it. Fixes #107 Signed-off-by: choksi2212 --- src/joserfc/_rfc7519/claims.py | 25 +++++++++++++++---------- tests/jwt/test_claims.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/joserfc/_rfc7519/claims.py b/src/joserfc/_rfc7519/claims.py index e35c973..1d1421e 100644 --- a/src/joserfc/_rfc7519/claims.py +++ b/src/joserfc/_rfc7519/claims.py @@ -31,7 +31,7 @@ def convert_claims(claims: Claims, encoder_cls: type[JSONEncoder] | None = None) class ClaimsOption(TypedDict, total=False): essential: bool allow_blank: bool | None - value: str | int | bool + value: str | int | bool | list[str | int | bool] values: list[str | int | bool] | list[str] | list[int] | list[bool] @@ -64,20 +64,25 @@ def check_value(self, claim_name: str, value: Any) -> None: option_values = option.get("values") - if option_values is None: - option_value = option.get("value") - if option_value is not None: - option_values = [option_value] + if option_values: + if isinstance(value, list): + if not any(v in value for v in option_values): + raise InvalidClaimError(claim_name) + elif value not in option_values: + raise InvalidClaimError(claim_name) + return - if not option_values: + option_value = option.get("value") + if option_value is None: return + # "value" is the expected claim value itself; a list claim also + # matches when one of its elements is the expected value if isinstance(value, list): - if not any(v in value for v in option_values): - raise InvalidClaimError(claim_name) - else: - if value not in option_values: + if value != option_value and option_value not in value: raise InvalidClaimError(claim_name) + elif value != option_value: + raise InvalidClaimError(claim_name) def validate(self, claims: dict[str, Any]) -> None: """ diff --git a/tests/jwt/test_claims.py b/tests/jwt/test_claims.py index d5bf46a..fb6d12e 100644 --- a/tests/jwt/test_claims.py +++ b/tests/jwt/test_claims.py @@ -129,6 +129,22 @@ def test_option_value(self): claims_requests.validate({"foo": False}) self.assertRaises(InvalidClaimError, claims_requests.validate, {"foo": True}) + def test_option_value_with_list(self): + # a list "value" is the expected claim value itself + claims_requests = jwt.JWTClaimsRegistry(aud={"essential": True, "value": ["id1"]}) + claims_requests.validate({"aud": ["id1"]}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"aud": ["id2"]}) + + claims_requests = jwt.JWTClaimsRegistry(aud={"essential": True, "value": ["id1", "id2"]}) + claims_requests.validate({"aud": ["id1", "id2"]}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"aud": "id1"}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"aud": ["id1"]}) + + # a scalar "value" still matches any element of a list claim + claims_requests = jwt.JWTClaimsRegistry(foo={"essential": True, "value": "id1"}) + claims_requests.validate({"foo": ["id1"]}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"foo": ["id2"]}) + def test_option_values(self): claims_requests = jwt.JWTClaimsRegistry(foo={"essential": True, "values": ["1", "2", True, False]}) self.assertRaises(InvalidClaimError, claims_requests.validate, {"foo": "a"})