From e935bc4d32c0b95794e12c2f484820e85f4db99e Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sun, 22 Mar 2026 11:52:42 +0100 Subject: [PATCH] fix: accept ES256 (ECC P-256) JWT keys alongside RS256 in backend auth Supabase JWT key was switched to ECC P-256, but the JWKS verification only accepted RS256. Add ES256 to the accepted algorithms list. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...s256-ecc-p-256-jwt-keys-in-backend-auth.md | 10 ++++ backend/src/app/core/auth.py | 4 +- backend/tests/test_auth.py | 51 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 .beans/nuzlocke-tracker-snft--support-es256-ecc-p-256-jwt-keys-in-backend-auth.md diff --git a/.beans/nuzlocke-tracker-snft--support-es256-ecc-p-256-jwt-keys-in-backend-auth.md b/.beans/nuzlocke-tracker-snft--support-es256-ecc-p-256-jwt-keys-in-backend-auth.md new file mode 100644 index 0000000..61452e3 --- /dev/null +++ b/.beans/nuzlocke-tracker-snft--support-es256-ecc-p-256-jwt-keys-in-backend-auth.md @@ -0,0 +1,10 @@ +--- +# nuzlocke-tracker-snft +title: Support ES256 (ECC P-256) JWT keys in backend auth +status: in-progress +type: bug +created_at: 2026-03-22T10:51:30Z +updated_at: 2026-03-22T10:51:30Z +--- + +Backend JWKS verification only accepts RS256 algorithm, but Supabase JWT key was switched to ECC P-256 (ES256). This causes 401 errors on all authenticated requests. Fix: accept both RS256 and ES256 in the algorithms list, and update tests accordingly. diff --git a/backend/src/app/core/auth.py b/backend/src/app/core/auth.py index 4446a3f..77fc491 100644 --- a/backend/src/app/core/auth.py +++ b/backend/src/app/core/auth.py @@ -60,7 +60,7 @@ def _verify_jwt_hs256(token: str) -> dict | None: def _verify_jwt(token: str) -> dict | None: - """Verify JWT using JWKS (RS256), falling back to HS256 shared secret.""" + """Verify JWT using JWKS (RS256/ES256), falling back to HS256 shared secret.""" client = _get_jwks_client() if client: try: @@ -68,7 +68,7 @@ def _verify_jwt(token: str) -> dict | None: return jwt.decode( token, signing_key.key, - algorithms=["RS256"], + algorithms=["RS256", "ES256"], audience="authenticated", ) except jwt.InvalidTokenError: diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index a37cb78..a49fc78 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -4,7 +4,7 @@ from uuid import UUID import jwt import pytest -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import ec, rsa from httpx import ASGITransport, AsyncClient from app.core.auth import AuthUser, get_current_user, require_admin, require_auth @@ -73,6 +73,55 @@ def mock_jwks_client(rsa_key_pair): return mock_client +@pytest.fixture(scope="module") +def ec_key_pair(): + """Generate EC P-256 key pair for testing.""" + private_key = ec.generate_private_key(ec.SECP256R1()) + public_key = private_key.public_key() + return private_key, public_key + + +@pytest.fixture +def valid_es256_token(ec_key_pair): + """Generate a valid ES256 JWT token.""" + private_key, _ = ec_key_pair + payload = { + "sub": "user-456", + "email": "ec-user@example.com", + "role": "authenticated", + "aud": "authenticated", + "exp": int(time.time()) + 3600, + } + return jwt.encode(payload, private_key, algorithm="ES256") + + +@pytest.fixture +def mock_jwks_client_ec(ec_key_pair): + """Create a mock JWKS client that returns our test EC public key.""" + _, public_key = ec_key_pair + mock_client = MagicMock() + mock_signing_key = MagicMock() + mock_signing_key.key = public_key + mock_client.get_signing_key_from_jwt.return_value = mock_signing_key + return mock_client + + +async def test_get_current_user_valid_es256_token( + valid_es256_token, mock_jwks_client_ec +): + """Test get_current_user works with ES256 (ECC P-256) tokens.""" + with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client_ec): + + class MockRequest: + headers = {"Authorization": f"Bearer {valid_es256_token}"} + + user = get_current_user(MockRequest()) + assert user is not None + assert user.id == "user-456" + assert user.email == "ec-user@example.com" + assert user.role == "authenticated" + + async def test_get_current_user_valid_token(valid_token, mock_jwks_client): """Test get_current_user returns user for valid token.""" with patch("app.core.auth._get_jwks_client", return_value=mock_jwks_client):