Compare commits
2 Commits
0ec1beac8f
...
d98b0da410
| Author | SHA1 | Date | |
|---|---|---|---|
| d98b0da410 | |||
| af55cdd8a6 |
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
return jwt.decode(
|
||||||
|
token,
|
||||||
|
settings.supabase_jwt_secret,
|
||||||
|
algorithms=["HS256"],
|
||||||
|
audience="authenticated",
|
||||||
|
)
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
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:
|
try:
|
||||||
signing_key = client.get_signing_key_from_jwt(token)
|
signing_key = client.get_signing_key_from_jwt(token)
|
||||||
payload = jwt.decode(
|
return jwt.decode(
|
||||||
token,
|
token,
|
||||||
signing_key.key,
|
signing_key.key,
|
||||||
algorithms=["RS256"],
|
algorithms=["RS256"],
|
||||||
audience="authenticated",
|
audience="authenticated",
|
||||||
)
|
)
|
||||||
return payload
|
except jwt.InvalidTokenError, PyJWKClientError:
|
||||||
except jwt.ExpiredSignatureError:
|
pass
|
||||||
return None
|
return _verify_jwt_hs256(token)
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return None
|
|
||||||
except PyJWKClientError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_user(request: Request) -> AuthUser | None:
|
def get_current_user(request: Request) -> AuthUser | None:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user