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