Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
9 changes: 2 additions & 7 deletions docs/guide/jwt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions requirements-docs.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ 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
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
Expand All @@ -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.*'
Expand Down
13 changes: 6 additions & 7 deletions src/joserfc/_rfc7519/claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
MissingClaimError,
InvalidClaimError,
ExpiredTokenError,
InvalidTokenError,
)

Claims = dict[str, Any]
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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)


Expand Down
18 changes: 8 additions & 10 deletions src/joserfc/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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
37 changes: 17 additions & 20 deletions tests/jwt/test_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
InvalidClaimError,
MissingClaimError,
ExpiredTokenError,
InvalidTokenError,
)


Expand Down Expand Up @@ -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"})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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})