From b71ca0f75d2486e723ad4e86070dbca30bc839f3 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 16 Dec 2025 16:57:27 +0900 Subject: [PATCH 1/2] fix(jwt): remove InvalidTokenError, ExpiredTokenError based on ClaimError --- src/joserfc/_rfc7519/claims.py | 13 ++++++------ src/joserfc/errors.py | 18 ++++++++--------- tests/jwt/test_claims.py | 37 ++++++++++++++++------------------ 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/joserfc/_rfc7519/claims.py b/src/joserfc/_rfc7519/claims.py index 342286c9..08d61458 100644 --- a/src/joserfc/_rfc7519/claims.py +++ b/src/joserfc/_rfc7519/claims.py @@ -10,7 +10,6 @@ MissingClaimError, InvalidClaimError, ExpiredTokenError, - InvalidTokenError, ) Claims = dict[str, Any] @@ -132,9 +131,9 @@ def validate_exp(self, value: int) -> None: containing a NumericDate value. Use of this claim is OPTIONAL. """ if not _validate_numeric_time(value): - raise InvalidClaimError("exp") + raise InvalidClaimError("exp", "Claim 'exp' must be a NumericDate value") if value < (self.now - self.leeway): - raise ExpiredTokenError() + raise ExpiredTokenError("exp") self.check_value("exp", value) def validate_nbf(self, value: int) -> None: @@ -147,9 +146,9 @@ def validate_nbf(self, value: int) -> None: NumericDate value. Use of this claim is OPTIONAL. """ if not _validate_numeric_time(value): - raise InvalidClaimError("nbf") + raise InvalidClaimError("nbf", "Claim 'nbf' must be a NumericDate value") if value > (self.now + self.leeway): - raise InvalidTokenError() + raise InvalidClaimError("nbf", "The token is not yet valid") self.check_value("nbf", value) def validate_iat(self, value: int) -> None: @@ -159,9 +158,9 @@ def validate_iat(self, value: int) -> None: claim is OPTIONAL. """ if not _validate_numeric_time(value): - raise InvalidClaimError("iat") + raise InvalidClaimError("iat", "Claim 'iat' must be a NumericDate value") if value > (self.now + self.leeway): - raise InvalidTokenError() + raise InvalidClaimError("iat", "The token was issued in the future") self.check_value("iat", value) diff --git a/src/joserfc/errors.py b/src/joserfc/errors.py index bcc8bedb..0d9650f0 100644 --- a/src/joserfc/errors.py +++ b/src/joserfc/errors.py @@ -201,9 +201,10 @@ class ClaimError(JoseError): claim: str description = "Error claim: '{}'" - def __init__(self, claim: str): + def __init__(self, claim: str, description: str | None = None): self.claim = claim - description = self.description.format(claim) + if description is None: + description = self.description.format(claim) super(ClaimError, self).__init__(description=description) @@ -231,22 +232,19 @@ class InsecureClaimError(ClaimError): description = "Insecure claim: '{}'" -class ExpiredTokenError(JoseError): +class ExpiredTokenError(ClaimError): """This error is designed for JWT. It raised when the token is expired.""" error = "expired_token" description = "The token is expired" -class InvalidTokenError(JoseError): - """This error is designed for JWT. It raised when the token is not valid yet.""" - - error = "invalid_token" - description = "The token is not valid yet" - - class InvalidPayloadError(JoseError): """This error is designed for JWT. It raised when the payload is not a valid JSON object.""" error = "invalid_payload" + + +# compatibility +InvalidTokenError = InvalidClaimError diff --git a/tests/jwt/test_claims.py b/tests/jwt/test_claims.py index fbbfb857..727bfea1 100644 --- a/tests/jwt/test_claims.py +++ b/tests/jwt/test_claims.py @@ -10,7 +10,6 @@ InvalidClaimError, MissingClaimError, ExpiredTokenError, - InvalidTokenError, ) @@ -90,7 +89,7 @@ def test_int_claims(self): claims_requests.validate({"exp": now + 100, "nbf": now - 100, "iat": now}) self.assertRaises(ExpiredTokenError, claims_requests.validate, {"exp": now - 100}) - self.assertRaises(InvalidTokenError, claims_requests.validate, {"nbf": now + 100}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"nbf": now + 100}) def test_validate_aud(self): claims_requests = jwt.JWTClaimsRegistry(aud={"essential": True, "value": "a"}) @@ -129,13 +128,13 @@ def test_validate_iat(self): claims_requests = jwt.JWTClaimsRegistry(leeway=500) now = int(time.time()) claims_requests.validate({"iat": now}) - self.assertRaises(InvalidTokenError, claims_requests.validate, {"iat": now + 1000}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"iat": now + 1000}) def test_validate_nbf(self): claims_requests = jwt.JWTClaimsRegistry(leeway=500) now = int(time.time()) claims_requests.validate({"nbf": now}) - self.assertRaises(InvalidTokenError, claims_requests.validate, {"nbf": now + 1000}) + self.assertRaises(InvalidClaimError, claims_requests.validate, {"nbf": now + 1000}) def test_claims_with_uuid_field(self): value = uuid.uuid4() @@ -175,30 +174,28 @@ def test_validate_list_inclusion(self): claims_requests.validate({"custom_claim": "a"}) def test_validate_allow_blank(self): + blank_values = ["", [], {}] + # Case 1: allow blank value claims_requests = jwt.JWTClaimsRegistry(custom_claim={"essential": True, "allow_blank": True}) self.assertRaises(MissingClaimError, claims_requests.validate, {"custom_claim": None}) - claims_requests.validate({"custom_claim": ""}) - claims_requests.validate({"custom_claim": []}) - claims_requests.validate({"custom_claim": {}}) + for value in blank_values: + claims_requests.validate({"custom_claim": value}) # Case 2: allow blank value without essential claims_requests = jwt.JWTClaimsRegistry(custom_claim={"allow_blank": True}) claims_requests.validate({"custom_claim": None}) - claims_requests.validate({"custom_claim": ""}) - claims_requests.validate({"custom_claim": []}) - claims_requests.validate({"custom_claim": {}}) + for value in blank_values: + claims_requests.validate({"custom_claim": value}) # Case 3: do not allow blank value - claims_requests = jwt.JWTClaimsRegistry(custom_claim={"essential": True, "allow_blank": False}) - self.assertRaises(MissingClaimError, claims_requests.validate, {"custom_claim": None}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": ""}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": []}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": {}}) - - # Case 4: do not allow blank value without essential claims_requests = jwt.JWTClaimsRegistry(custom_claim={"allow_blank": False}) self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": None}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": ""}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": []}) - self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": {}}) + for value in blank_values: + self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": value}) + + # Case 4: do not allow blank value on essential claim + claims_requests = jwt.JWTClaimsRegistry(custom_claim={"essential": True, "allow_blank": False}) + self.assertRaises(MissingClaimError, claims_requests.validate, {"custom_claim": None}) + for value in blank_values: + self.assertRaises(InvalidClaimError, claims_requests.validate, {"custom_claim": value}) From c383fe6f4be7162bcb66d31217d35dee452a83a4 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 16 Dec 2025 17:03:16 +0900 Subject: [PATCH 2/2] docs: cleanup docs on JWT --- .github/workflows/test.yml | 2 +- docs/guide/jwt.rst | 9 ++------- requirements-dev.lock | 4 ++-- requirements-docs.lock | 9 +++++---- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e86c4e1..438b6260 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,7 +83,7 @@ jobs: name: GitHub - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v6 + uses: SonarSource/sonarqube-scan-action@v7 continue-on-error: true env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/docs/guide/jwt.rst b/docs/guide/jwt.rst index 5d7d27fd..fe5c5ab3 100644 --- a/docs/guide/jwt.rst +++ b/docs/guide/jwt.rst @@ -108,7 +108,7 @@ JSON object. .. code-block:: python - from joserfc.errors import ClaimError, ExpiredTokenError, InvalidTokenError + from joserfc.errors import ClaimError from joserfc.jwt import JWTClaimsRegistry claims_requests = JWTClaimsRegistry( @@ -119,12 +119,7 @@ JSON object. try: claims_requests.validate(token.claims) except ClaimError as error: - print(error) - except ExpiredTokenError as error: - print("expired") - except InvalidTokenError as error: - # only happens when iat and nbf invalid - print("invalid") + print(error.claim, error.error, error.description) The Individual Claims Requests JSON object contains: diff --git a/requirements-dev.lock b/requirements-dev.lock index eb1f1390..1f79d806 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -11,11 +11,11 @@ cryptography==46.0.3 distlib==0.4.0 exceptiongroup==1.3.1 ; python_full_version < '3.11' filelock==3.19.1 ; python_full_version < '3.10' -filelock==3.20.0 ; python_full_version >= '3.10' +filelock==3.20.1 ; python_full_version >= '3.10' identify==2.6.15 iniconfig==2.1.0 ; python_full_version < '3.10' iniconfig==2.3.0 ; python_full_version >= '3.10' -librt==0.7.3 ; platform_python_implementation != 'PyPy' +librt==0.7.4 ; platform_python_implementation != 'PyPy' mypy==1.19.1 mypy-extensions==1.1.0 nodeenv==1.9.1 diff --git a/requirements-docs.lock b/requirements-docs.lock index e6c2e466..7732838e 100644 --- a/requirements-docs.lock +++ b/requirements-docs.lock @@ -19,7 +19,7 @@ distlib==0.4.0 docutils==0.21.2 exceptiongroup==1.3.1 ; python_full_version < '3.11' filelock==3.19.1 ; python_full_version < '3.10' -filelock==3.20.0 ; python_full_version >= '3.10' +filelock==3.20.1 ; python_full_version >= '3.10' identify==2.6.15 idna==3.11 imagesize==1.4.1 @@ -27,7 +27,7 @@ importlib-metadata==8.7.0 ; python_full_version < '3.10' iniconfig==2.1.0 ; python_full_version < '3.10' iniconfig==2.3.0 ; python_full_version >= '3.10' jinja2==3.1.6 -librt==0.7.3 ; platform_python_implementation != 'PyPy' +librt==0.7.4 ; platform_python_implementation != 'PyPy' markupsafe==3.0.3 mypy==1.19.1 mypy-extensions==1.1.0 @@ -48,10 +48,11 @@ pytest==9.0.2 ; python_full_version >= '3.10' pytest-cov==7.0.0 pyyaml==6.0.3 requests==2.32.5 -roman-numerals-py==3.1.0 ; python_full_version >= '3.11' +roman-numerals==4.0.0 ; python_full_version >= '3.11' +roman-numerals-py==4.0.0 ; python_full_version >= '3.11' ruff==0.14.9 shibuya==2025.10.21 ; python_full_version < '3.10' -shibuya==2025.12.14 ; python_full_version >= '3.10' +shibuya==2025.12.16 ; python_full_version >= '3.10' snowballstemmer==3.0.1 sphinx==7.4.7 ; python_full_version < '3.10' sphinx==8.1.3 ; python_full_version == '3.10.*'