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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.1.0] - 2026-03-10

- Add support for ES* algorithms for keys.
- Add support for ES* algorithms (`ES256`, `ES384`, `ES512`) for EC keys in
`JWK`, `JWKS`, and `AsymmetricJWTValidator`.
- Fix flaky test `test_jwt_validator_fetches_tokens_again_for_unknown_kid`
that failed on slower hardware (e.g. Raspberry Pi / aarch64) due to a
timing-sensitive `refresh_time` threshold; replaced real-time sleeps with
mocked time for deterministic behaviour. Reported by
[@wrobell](https://github.com/wrobell) in
[#18](https://github.com/Neoteroi/GuardPost/issues/18).

## [1.0.4] - 2025-10-18 :musical_keyboard:

Expand Down
62 changes: 35 additions & 27 deletions tests/test_jwts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time
from typing import Any, Dict, Iterable
from unittest.mock import patch

import jwt
import pytest
Expand Down Expand Up @@ -74,41 +74,49 @@ async def test_jwt_validator_can_validate_valid_access_tokens(default_keys_provi

@pytest.mark.asyncio
async def test_jwt_validator_cache_expiration(default_keys_provider):
validator = JWTValidator(
valid_audiences=["a"],
valid_issuers=["b"],
keys_provider=default_keys_provider,
cache_time=0.1,
)
await _valid_tokens_scenario(validator)
time.sleep(0.2)
await _valid_tokens_scenario(validator)
with patch("guardpost.jwks.caching.time") as mock_time:
mock_time.time.return_value = 0
validator = JWTValidator(
valid_audiences=["a"],
valid_issuers=["b"],
keys_provider=default_keys_provider,
cache_time=10,
)
await _valid_tokens_scenario(validator)

# Simulate cache_time elapsed — keys must be re-fetched
mock_time.time.return_value = 11
await _valid_tokens_scenario(validator)


@pytest.mark.asyncio
async def test_jwt_validator_fetches_tokens_again_for_unknown_kid():
keys = get_test_jwks()
# configure a key provider that returns the given JWKS in sequence
keys_provider = MockedKeysProvider([JWKS(keys.keys[0:2]), JWKS(keys.keys[2:])])
validator = JWTValidator(
valid_audiences=["a"],
valid_issuers=["b"],
keys_provider=keys_provider,
cache_time=10,
refresh_time=0.2,
)
await _valid_token_scenario("0", validator)
await _valid_token_scenario("1", validator)

# this must fail because tokens were just fetched, and kid "2" is not present
with pytest.raises(InvalidAccessToken):
with patch("guardpost.jwks.caching.time") as mock_time:
mock_time.time.return_value = 0
validator = JWTValidator(
valid_audiences=["a"],
valid_issuers=["b"],
keys_provider=keys_provider,
cache_time=10,
refresh_time=30,
)
await _valid_token_scenario("0", validator)
await _valid_token_scenario("1", validator)

# this must fail because refresh_time has not elapsed yet (t=1 < 30s)
mock_time.time.return_value = 1
with pytest.raises(InvalidAccessToken):
await _valid_token_scenario("2", validator)

# simulate refresh_time elapsed — provider should now fetch the new keys
mock_time.time.return_value = 31
await _valid_token_scenario("2", validator)

time.sleep(0.3)
# now the JWTValidator should fetch automatically the new keys
await _valid_token_scenario("2", validator)
await _valid_token_scenario("3", validator)
await _valid_token_scenario("4", validator)
await _valid_token_scenario("3", validator)
await _valid_token_scenario("4", validator)


@pytest.mark.asyncio
Expand Down