Skip to content

Commit db1707d

Browse files
authored
Merge pull request #1199 from MatthewJamisonJS/fix/joserfc-migration-1197
fix: migrate authlib.jose to joserfc (#1197)
2 parents 786db5c + 1f09036 commit db1707d

8 files changed

Lines changed: 99 additions & 14 deletions

File tree

carbonserver/carbonserver/api/services/auth_providers/oidc_auth_provider.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
from typing import Any, Dict, Optional, Tuple
1010

1111
from authlib.integrations.starlette_client import OAuth
12-
from authlib.jose import JsonWebKey
13-
from authlib.jose import jwt as jose_jwt
1412
from fastapi import Response
1513
from fief_client import FiefAsync
14+
from joserfc import jwt as jose_jwt
15+
from joserfc.jwk import KeySet
1616

1717
from carbonserver.config import settings
1818

@@ -63,10 +63,10 @@ async def _decode_token(self, token: str) -> Dict[str, Any]:
6363
...
6464

6565
jwks_data = await self.client.fetch_jwk_set()
66-
keyset = JsonWebKey.import_key_set(jwks_data)
67-
claims = jose_jwt.decode(token, keyset)
68-
claims.validate()
69-
return dict(claims)
66+
keyset = KeySet.import_key_set(jwks_data)
67+
decoded = jose_jwt.decode(token, keyset)
68+
jose_jwt.JWTClaimsRegistry().validate(decoded.claims)
69+
return dict(decoded.claims)
7070

7171
async def validate_access_token(self, token: str) -> bool:
7272
await self._decode_token(token)

carbonserver/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies = [
3737
"PyJWT",
3838
"fastapi-oidc>=0.0.9",
3939
"authlib>=1.6.6",
40+
"joserfc>=1.0.0",
4041
"itsdangerous>=2.2.0",
4142
]
4243

carbonserver/tests/api/service/test_auth_provider.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Unit tests for OIDC authentication provider.
33
"""
44

5+
import time
6+
from unittest.mock import AsyncMock, MagicMock, patch
7+
8+
import pytest
9+
10+
from carbonserver.api.services.auth_providers import oidc_auth_provider
511
from carbonserver.api.services.auth_providers.oidc_auth_provider import OIDCAuthProvider
612
from carbonserver.config import settings
713

@@ -26,3 +32,52 @@ def test_oidc_provider_initialization(self):
2632
settings.oidc_client_id,
2733
settings.oidc_client_secret,
2834
)
35+
36+
@pytest.mark.asyncio
37+
async def test_decode_token_falls_back_to_jwks_when_fief_fails(self):
38+
"""When fief.validate_access_token raises, _decode_token must fall back
39+
to the joserfc JWKS verification path and return a plain dict."""
40+
provider = OIDCAuthProvider(
41+
base_url="https://auth.example.com",
42+
client_id="test_client",
43+
client_secret="test_secret",
44+
)
45+
46+
now = int(time.time())
47+
expected_claims = {
48+
"sub": "user-456",
49+
"iat": now - 5,
50+
"exp": now + 600,
51+
"email": "user@example.com",
52+
}
53+
54+
jwks_payload = {"keys": [{"kty": "RSA", "kid": "k1"}]}
55+
provider.client = MagicMock()
56+
provider.client.fetch_jwk_set = AsyncMock(return_value=jwks_payload)
57+
58+
decoded_token = MagicMock()
59+
decoded_token.claims = expected_claims
60+
61+
with (
62+
patch.object(
63+
oidc_auth_provider.fief,
64+
"validate_access_token",
65+
new=AsyncMock(side_effect=Exception("fief unavailable")),
66+
),
67+
patch.object(
68+
oidc_auth_provider.KeySet,
69+
"import_key_set",
70+
return_value="keyset",
71+
) as mock_import,
72+
patch.object(
73+
oidc_auth_provider.jose_jwt,
74+
"decode",
75+
return_value=decoded_token,
76+
) as mock_decode,
77+
):
78+
result = await provider._decode_token("opaque-token")
79+
80+
assert result == expected_claims
81+
assert isinstance(result, dict)
82+
mock_import.assert_called_once_with(jwks_payload)
83+
mock_decode.assert_called_once_with("opaque-token", "keyset")

carbonserver/uv.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codecarbon/cli/auth.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
import requests
1717
from authlib.common.security import generate_token
1818
from authlib.integrations.requests_client import OAuth2Session
19-
from authlib.jose import JsonWebKey
20-
from authlib.jose import jwt as jose_jwt
2119
from authlib.oauth2.rfc7636 import create_s256_code_challenge
20+
from joserfc import jwt as jose_jwt
21+
from joserfc.jwk import KeySet
2222

2323
AUTH_CLIENT_ID = os.environ.get(
2424
"AUTH_CLIENT_ID",
@@ -110,9 +110,9 @@ def _validate_access_token(access_token: str) -> bool:
110110
discovery = _discover_endpoints()
111111
jwks_resp = requests.get(discovery["jwks_uri"])
112112
jwks_resp.raise_for_status()
113-
keyset = JsonWebKey.import_key_set(jwks_resp.json())
114-
claims = jose_jwt.decode(access_token, keyset)
115-
claims.validate()
113+
keyset = KeySet.import_key_set(jwks_resp.json())
114+
token = jose_jwt.decode(access_token, keyset)
115+
jose_jwt.JWTClaimsRegistry().validate(token.claims)
116116
return True
117117
except requests.RequestException as exc:
118118
logger.warning(

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ classifiers = [
2727
dependencies = [
2828
"arrow",
2929
"authlib>=1.2.1",
30+
"joserfc>=1.0.0",
3031
"click",
3132
"pandas>=2.3.3;python_version>='3.14'",
3233
"pandas;python_version<'3.14'",

tests/cli/test_cli_auth.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
import json
33
import tempfile
4+
import time
45
import unittest
56
from pathlib import Path
67
from unittest.mock import MagicMock, patch
@@ -90,20 +91,42 @@ def test_save_and_load_credentials(self, mock_open):
9091
self.assertEqual(loaded, tokens)
9192

9293
@patch("codecarbon.cli.auth.requests.get")
93-
@patch("codecarbon.cli.auth.JsonWebKey.import_key_set")
94+
@patch("codecarbon.cli.auth.KeySet.import_key_set")
9495
@patch("codecarbon.cli.auth.jose_jwt.decode")
9596
def test_validate_access_token_valid(
9697
self, mock_decode, mock_import_key_set, mock_get
9798
):
9899
mock_get.return_value.json.return_value = {"jwks_uri": "jwks"}
99100
mock_get.return_value.raise_for_status.return_value = None
100101
mock_import_key_set.return_value = "keyset"
101-
mock_decode.return_value.validate.return_value = None
102+
now = int(time.time())
103+
mock_decode.return_value.claims = {
104+
"iat": now - 10,
105+
"exp": now + 300,
106+
"sub": "user-123",
107+
}
102108
with patch(
103109
"codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"}
104110
):
105111
self.assertTrue(auth._validate_access_token("token"))
106112

113+
@patch("codecarbon.cli.auth.requests.get")
114+
@patch("codecarbon.cli.auth.KeySet.import_key_set")
115+
@patch("codecarbon.cli.auth.jose_jwt.decode")
116+
def test_validate_access_token_expired_returns_false(
117+
self, mock_decode, mock_import_key_set, mock_get
118+
):
119+
# Expired exp must trip JWTClaimsRegistry validation
120+
mock_get.return_value.json.return_value = {"jwks_uri": "jwks"}
121+
mock_get.return_value.raise_for_status.return_value = None
122+
mock_import_key_set.return_value = "keyset"
123+
now = int(time.time())
124+
mock_decode.return_value.claims = {"exp": now - 10}
125+
with patch(
126+
"codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"}
127+
):
128+
self.assertFalse(auth._validate_access_token("token"))
129+
107130
@patch("codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"})
108131
@patch(
109132
"codecarbon.cli.auth.requests.get",
@@ -119,7 +142,7 @@ def test_validate_access_token_network_error_returns_true(
119142

120143
@patch("codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"})
121144
@patch("codecarbon.cli.auth.requests.get")
122-
@patch("codecarbon.cli.auth.JsonWebKey.import_key_set")
145+
@patch("codecarbon.cli.auth.KeySet.import_key_set")
123146
@patch(
124147
"codecarbon.cli.auth.jose_jwt.decode",
125148
side_effect=Exception("invalid"),

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)