diff --git a/.beans/nuzlocke-tracker-eg7j--fix-jwt-verification-failing-in-local-dev-hs256-fa.md b/.beans/nuzlocke-tracker-eg7j--fix-jwt-verification-failing-in-local-dev-hs256-fa.md new file mode 100644 index 0000000..e29a341 --- /dev/null +++ b/.beans/nuzlocke-tracker-eg7j--fix-jwt-verification-failing-in-local-dev-hs256-fa.md @@ -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. diff --git a/.beans/nuzlocke-tracker-lkro--ux-make-team-section-a-floating-sidebar-on-desktop.md b/.beans/nuzlocke-tracker-lkro--ux-make-team-section-a-floating-sidebar-on-desktop.md index 10c8439..d1ede42 100644 --- a/.beans/nuzlocke-tracker-lkro--ux-make-team-section-a-floating-sidebar-on-desktop.md +++ b/.beans/nuzlocke-tracker-lkro--ux-make-team-section-a-floating-sidebar-on-desktop.md @@ -5,11 +5,7 @@ status: todo type: feature priority: normal created_at: 2026-03-21T21:50:48Z -<<<<<<< Updated upstream -updated_at: 2026-03-21T22:04:08Z -======= updated_at: 2026-03-22T08:08:13Z ->>>>>>> Stashed changes --- ## Problem diff --git a/.env.example b/.env.example index aba12cf..5e15c1c 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,8 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke # Supabase Auth (backend uses JWKS from this URL for JWT verification) # For local dev with GoTrue container: 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 # For production, replace with your Supabase cloud values: # SUPABASE_URL=https://your-project.supabase.co diff --git a/backend/.env.example b/backend/.env.example index 9b444f2..fbe78f0 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -11,3 +11,5 @@ DATABASE_URL="sqlite:///./nuzlocke.db" # Supabase Auth (JWKS used for JWT verification) SUPABASE_URL=https://your-project.supabase.co 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 diff --git a/backend/src/app/core/auth.py b/backend/src/app/core/auth.py index aa6172a..d717aca 100644 --- a/backend/src/app/core/auth.py +++ b/backend/src/app/core/auth.py @@ -44,26 +44,36 @@ def _extract_token(request: Request) -> str | None: return parts[1] -def _verify_jwt(token: str) -> dict | None: - """Verify JWT using JWKS public key. Returns payload or None.""" - client = _get_jwks_client() - if not client: +def _verify_jwt_hs256(token: str) -> dict | None: + """Verify JWT using HS256 shared secret. Returns payload or None.""" + if not settings.supabase_jwt_secret: return None try: - signing_key = client.get_signing_key_from_jwt(token) - payload = jwt.decode( + return jwt.decode( token, - signing_key.key, - algorithms=["RS256"], + settings.supabase_jwt_secret, + algorithms=["HS256"], audience="authenticated", ) - return payload - except jwt.ExpiredSignatureError: - return None except jwt.InvalidTokenError: 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: diff --git a/backend/src/app/core/config.py b/backend/src/app/core/config.py index 84541c3..7ef08af 100644 --- a/backend/src/app/core/config.py +++ b/backend/src/app/core/config.py @@ -20,6 +20,7 @@ class Settings(BaseSettings): # Supabase Auth supabase_url: str | None = None supabase_anon_key: str | None = None + supabase_jwt_secret: str | None = None settings = Settings() diff --git a/docker-compose.yml b/docker-compose.yml index dae4909..09d43ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,8 +12,9 @@ services: environment: - DEBUG=true - 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_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long depends_on: db: condition: service_healthy