diff --git a/CHANGELOG.md b/CHANGELOG.md index b6befc8..8e7ec85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## Upcoming Changes + +#### 🐛 Bug Fixes + +- **Reject non-finite DID token `iat` / `ext` / `nbf`** + - `json.loads` accepts `NaN` and `Infinity`. Comparisons against those values are always false, so `ext: Infinity` never expired and `nbf: NaN` never blocked use. + - Strings such as `"never"` now raise `DIDTokenMalformed` instead of a raw `TypeError`. + +--- + ## `2.5.0` - 2026-06-19 #### 🚀 New Features diff --git a/magic_admin/resources/token.py b/magic_admin/resources/token.py index c0322e5..61c331f 100644 --- a/magic_admin/resources/token.py +++ b/magic_admin/resources/token.py @@ -1,5 +1,6 @@ import base64 import json +import math from eth_account.messages import encode_defunct from web3.auto import w3 @@ -17,6 +18,14 @@ EXPECTED_DID_TOKEN_CONTENT_LENGTH = 2 +def _is_finite_unix_seconds(value): + return ( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isfinite(value) + ) + + class Token(ResourceComponent): required_fields = frozenset( [ @@ -52,6 +61,14 @@ def _check_required_fields(cls, claim): ), ) + for field in ("iat", "ext", "nbf"): + if not _is_finite_unix_seconds(claim[field]): + raise DIDTokenMalformed( + message='DID token "{}" must be a finite Unix timestamp.'.format( + field, + ), + ) + @classmethod def decode(cls, did_token): """ @@ -148,6 +165,18 @@ def validate(cls, did_token): "with a suitable value.", ) + for field in ("ext", "nbf"): + if not _is_finite_unix_seconds(claim.get(field)): + raise DIDTokenMalformed( + message='DID token "{}" must be a finite Unix timestamp.'.format( + field, + ), + ) + if "iat" in claim and not _is_finite_unix_seconds(claim["iat"]): + raise DIDTokenMalformed( + message='DID token "iat" must be a finite Unix timestamp.', + ) + recovered_address = w3.eth.account.recover_message( encode_defunct( text=json.dumps(claim, separators=(",", ":")), diff --git a/tests/unit/resources/token_test.py b/tests/unit/resources/token_test.py index 6e44163..71a9358 100644 --- a/tests/unit/resources/token_test.py +++ b/tests/unit/resources/token_test.py @@ -18,7 +18,7 @@ class TestToken: @staticmethod def _generate_claim(fields=None): - return {field: mock.ANY for field in fields or Token.required_fields} + return {field: 1 for field in fields or Token.required_fields} def test_required_fields(self): assert ( @@ -39,6 +39,24 @@ def test_check_required_fields_raises_error(self): def test_check_required_fields_passes(self): Token._check_required_fields(self._generate_claim()) + def test_check_required_fields_rejects_non_finite_ext(self): + claim = self._generate_claim() + claim["ext"] = float("inf") + + with pytest.raises(DIDTokenMalformed) as e: + Token._check_required_fields(claim) + + assert str(e.value) == 'DID token "ext" must be a finite Unix timestamp.' + + def test_check_required_fields_rejects_nan_nbf(self): + claim = self._generate_claim() + claim["nbf"] = float("nan") + + with pytest.raises(DIDTokenMalformed) as e: + Token._check_required_fields(claim) + + assert str(e.value) == 'DID token "nbf" must be a finite Unix timestamp.' + def test_get_issuer_passes(self): mocked_claim = {"iss": self.issuer} @@ -286,6 +304,30 @@ def test_validate_raises_error_if_did_token_has_no_expiration(self, setup_mocks) " token with a suitable value." ) + def test_validate_raises_error_if_ext_is_not_finite(self, setup_mocks): + setup_mocks.claim["ext"] = float("inf") + + with pytest.raises(DIDTokenMalformed) as e: + Token.validate(self.did_token) + + assert str(e.value) == 'DID token "ext" must be a finite Unix timestamp.' + + def test_validate_raises_error_if_nbf_is_not_finite(self, setup_mocks): + setup_mocks.claim["nbf"] = float("nan") + + with pytest.raises(DIDTokenMalformed) as e: + Token.validate(self.did_token) + + assert str(e.value) == 'DID token "nbf" must be a finite Unix timestamp.' + + def test_validate_raises_error_if_ext_is_not_numeric(self, setup_mocks): + setup_mocks.claim["ext"] = "never" + + with pytest.raises(DIDTokenMalformed) as e: + Token.validate(self.did_token) + + assert str(e.value) == 'DID token "ext" must be a finite Unix timestamp.' + def test_validate_raises_error_if_did_token_used_before_nbf(self, setup_mocks): setup_mocks.epoch_time_now.return_value = setup_mocks.claim["nbf"] - 1