Merge pull request 'Fix JWT verification failing in local dev (HS256 fallback)' (#80) from feature/fix-jwt-verification-failing-in-local-dev-hs256-fallback into develop
All checks were successful
CI / backend-tests (push) Successful in 31s
CI / frontend-tests (push) Successful in 29s

Reviewed-on: #80
This commit was merged in pull request #80.
This commit is contained in:
2026-03-22 09:41:39 +01:00
7 changed files with 40 additions and 18 deletions

View File

@@ -0,0 +1,10 @@
---
# nuzlocke-tracker-eg7j
title: Fix JWT verification failing in local dev (HS256 fallback)
status: in-progress
type: bug
created_at: 2026-03-22T08:37:18Z
updated_at: 2026-03-22T08:37:18Z
---
Local GoTrue signs JWTs with HS256, but the JWKS migration only supports RS256. The JWKS endpoint returns empty keys locally, causing 500 errors on all authenticated endpoints. Add HS256 fallback using SUPABASE_JWT_SECRET for local dev.

View File

@@ -5,11 +5,7 @@ status: todo
type: feature type: feature
priority: normal priority: normal
created_at: 2026-03-21T21:50:48Z created_at: 2026-03-21T21:50:48Z
<<<<<<< Updated upstream
updated_at: 2026-03-21T22:04:08Z
=======
updated_at: 2026-03-22T08:08:13Z updated_at: 2026-03-22T08:08:13Z
>>>>>>> Stashed changes
--- ---
## Problem ## Problem

View File

@@ -5,6 +5,8 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke
# Supabase Auth (backend uses JWKS from this URL for JWT verification) # Supabase Auth (backend uses JWKS from this URL for JWT verification)
# For local dev with GoTrue container: # For local dev with GoTrue container:
SUPABASE_URL=http://localhost:9999 SUPABASE_URL=http://localhost:9999
# HS256 fallback for local GoTrue (not needed for Supabase Cloud):
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
# For production, replace with your Supabase cloud values: # For production, replace with your Supabase cloud values:
# SUPABASE_URL=https://your-project.supabase.co # SUPABASE_URL=https://your-project.supabase.co

View File

@@ -11,3 +11,5 @@ DATABASE_URL="sqlite:///./nuzlocke.db"
# Supabase Auth (JWKS used for JWT verification) # Supabase Auth (JWKS used for JWT verification)
SUPABASE_URL=https://your-project.supabase.co SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key SUPABASE_ANON_KEY=your-anon-key
# HS256 fallback for local GoTrue (not needed for Supabase Cloud):
# SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long

View File

@@ -44,26 +44,36 @@ def _extract_token(request: Request) -> str | None:
return parts[1] return parts[1]
def _verify_jwt(token: str) -> dict | None: def _verify_jwt_hs256(token: str) -> dict | None:
"""Verify JWT using JWKS public key. Returns payload or None.""" """Verify JWT using HS256 shared secret. Returns payload or None."""
client = _get_jwks_client() if not settings.supabase_jwt_secret:
if not client:
return None return None
try: try:
signing_key = client.get_signing_key_from_jwt(token) return jwt.decode(
payload = jwt.decode(
token, token,
signing_key.key, settings.supabase_jwt_secret,
algorithms=["RS256"], algorithms=["HS256"],
audience="authenticated", audience="authenticated",
) )
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return None return None
except PyJWKClientError:
return None
def _verify_jwt(token: str) -> dict | None:
"""Verify JWT using JWKS (RS256), falling back to HS256 shared secret."""
client = _get_jwks_client()
if client:
try:
signing_key = client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="authenticated",
)
except jwt.InvalidTokenError, PyJWKClientError:
pass
return _verify_jwt_hs256(token)
def get_current_user(request: Request) -> AuthUser | None: def get_current_user(request: Request) -> AuthUser | None:

View File

@@ -20,6 +20,7 @@ class Settings(BaseSettings):
# Supabase Auth # Supabase Auth
supabase_url: str | None = None supabase_url: str | None = None
supabase_anon_key: str | None = None supabase_anon_key: str | None = None
supabase_jwt_secret: str | None = None
settings = Settings() settings = Settings()

View File

@@ -12,8 +12,9 @@ services:
environment: environment:
- DEBUG=true - DEBUG=true
- DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke - DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke
# Auth - uses JWKS from GoTrue for JWT verification # Auth - uses JWKS from GoTrue for JWT verification, with HS256 fallback
- SUPABASE_URL=http://gotrue:9999 - SUPABASE_URL=http://gotrue:9999
- SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy