fix: add HS256 fallback for JWT verification in local dev
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 29s

Local GoTrue signs JWTs with HS256, but the JWKS endpoint returns an
empty key set since there are no RSA keys. Fall back to HS256 shared
secret verification when JWKS fails, using SUPABASE_JWT_SECRET.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 09:38:52 +01:00
parent 0ec1beac8f
commit af55cdd8a6
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
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

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)
# 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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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