Skip to content

Commit 064981e

Browse files
committed
chore: Resolves Issue #92, updates to use transitive dependency explicit
and removes the deprecation warning for the authlib.jose library.
1 parent 1a0a4c6 commit 064981e

4 files changed

Lines changed: 43 additions & 12 deletions

File tree

‎src/auth0_api_python/api_client.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Any, Optional, Union
55

66
import httpx
7-
from authlib.jose import JsonWebKey, JsonWebToken
7+
from joserfc import jwk, jwt
88

99
from .cache import InMemoryCache
1010
from .config import ApiClientOptions
@@ -60,6 +60,12 @@ def __init__(self, options: ApiClientOptions):
6060
if not options.audience:
6161
raise MissingRequiredArgumentError("audience")
6262

63+
if not isinstance(options.jwt_algorithms, list) or not options.jwt_algorithms or not all(
64+
isinstance(algorithm, str) and algorithm for algorithm in options.jwt_algorithms
65+
):
66+
raise ConfigurationError("jwt_algorithms must be a non-empty list of algorithm names")
67+
self._jwt_algorithms = options.jwt_algorithms
68+
6369
# Validate domains parameter if provided
6470
if options.domains is not None:
6571
if isinstance(options.domains, list):
@@ -113,10 +119,7 @@ def __init__(self, options: ApiClientOptions):
113119

114120
self._cache_ttl = options.cache_ttl_seconds
115121

116-
self._jwt = JsonWebToken(["RS256"])
117-
118122
self._dpop_algorithms = ["ES256"]
119-
self._dpop_jwt = JsonWebToken(self._dpop_algorithms)
120123

121124
def is_dpop_required(self) -> bool:
122125
"""Check if DPoP authentication is required."""
@@ -524,12 +527,12 @@ async def verify_access_token(
524527
raise VerifyAccessTokenError("No matching key found in JWKS")
525528

526529
# Import public key and verify signature
527-
public_key = JsonWebKey.import_key(matching_key_dict)
530+
public_key = jwk.import_key(matching_key_dict)
528531

529532
if isinstance(access_token, str) and access_token.startswith("b'"):
530533
access_token = access_token[2:-1]
531534
try:
532-
claims = self._jwt.decode(access_token, public_key)
535+
claims = jwt.decode(access_token, public_key, algorithms=self._jwt_algorithms).claims
533536
except Exception as e:
534537
raise VerifyAccessTokenError(f"Signature verification failed: {str(e)}") from e
535538

@@ -606,9 +609,9 @@ async def verify_dpop_proof(
606609
if jwk_dict.get("crv") != "P-256":
607610
raise InvalidDpopProofError("Only P-256 curve is supported")
608611

609-
public_key = JsonWebKey.import_key(jwk_dict)
612+
public_key = jwk.import_key(jwk_dict)
610613
try:
611-
claims = self._dpop_jwt.decode(proof, public_key)
614+
claims = jwt.decode(proof, public_key, algorithms=self._dpop_algorithms).claims
612615
except Exception as e:
613616
raise InvalidDpopProofError(f"JWT signature verification failed: {e}")
614617

‎src/auth0_api_python/config.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ApiClientOptions:
1919
Can be a static list of domain strings or a callable that returns
2020
allowed domains dynamically. Optional if domain is provided.
2121
audience: The expected 'aud' claim in the token.
22+
jwt_algorithms: Allowed access-token signing algorithms (default: ["RS256"]).
2223
custom_fetch: Optional callable that can replace the default HTTP fetch logic.
2324
cache_adapter: Custom cache implementation. If not provided, uses default InMemoryCache.
2425
cache_ttl_seconds: Time-to-live for cache entries in seconds (default: 600 = 10 minutes).
@@ -49,10 +50,12 @@ def __init__(
4950
client_id: Optional[str] = None,
5051
client_secret: Optional[str] = None,
5152
timeout: float = 10.0,
53+
jwt_algorithms: Optional[list[str]] = None,
5254
):
5355
self.domain = domain
5456
self.domains = domains
5557
self.audience = audience
58+
self.jwt_algorithms = ["RS256"] if jwt_algorithms is None else jwt_algorithms
5659
self.custom_fetch = custom_fetch
5760
self.cache_adapter = cache_adapter
5861
self.cache_ttl_seconds = cache_ttl_seconds

‎src/auth0_api_python/token_utils.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import uuid
33
from typing import Any, Optional, Union
44

5-
from authlib.jose import JsonWebKey, jwt
5+
from joserfc import jwk, jwt
6+
from joserfc.jws import JWSRegistry
7+
from joserfc.registry import HeaderParameter
68

79
from .utils import calculate_jwk_thumbprint, normalize_url_for_htu, sha256_base64url
810

@@ -80,7 +82,7 @@ async def generate_token(
8082
token_claims["aud"] = audience
8183

8284

83-
key = JsonWebKey.import_key(PRIVATE_JWK)
85+
key = jwk.import_key(PRIVATE_JWK)
8486

8587
header = {"alg": "RS256", "kid": PRIVATE_JWK["kid"]}
8688
token = jwt.encode(header, token_claims, key)
@@ -166,8 +168,15 @@ async def generate_dpop_proof(
166168
if header_overrides:
167169
header.update(header_overrides)
168170

169-
key = JsonWebKey.import_key(PRIVATE_EC_JWK)
170-
token = jwt.encode(header, proof_claims, key)
171+
key = jwk.import_key(PRIVATE_EC_JWK)
172+
registry = JWSRegistry(
173+
header_registry={
174+
name: HeaderParameter("test override", lambda _: None)
175+
for name in ("typ", "jwk")
176+
if name in (header_overrides or {})
177+
}
178+
)
179+
token = jwt.encode(header, proof_claims, key, registry=registry)
171180
# Ensure we return a string, not bytes
172181
return token.decode('utf-8') if isinstance(token, bytes) else token
173182

‎tests/test_api_client.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ async def test_init_missing_args():
6565
_ = ApiClient(ApiClientOptions(domain="example.us.auth0.com", audience=""))
6666

6767

68+
def test_custom_jwt_algorithms():
69+
client = ApiClient(ApiClientOptions(
70+
domain="example.us.auth0.com",
71+
audience="my-audience",
72+
jwt_algorithms=["RS384", "RS512"],
73+
))
74+
75+
assert client._jwt_algorithms == ["RS384", "RS512"]
76+
with pytest.raises(ConfigurationError):
77+
ApiClient(ApiClientOptions(
78+
domain="example.us.auth0.com",
79+
audience="my-audience",
80+
jwt_algorithms=[],
81+
))
82+
83+
6884
@pytest.mark.asyncio
6985
async def test_verify_access_token_successfully(httpx_mock: HTTPXMock):
7086
"""

0 commit comments

Comments
 (0)