Compare commits
10 Commits
d23e24b826
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c896075ead | ||
| ac0a04e71f | |||
| 94cc74c0fb | |||
| 41a18edb4f | |||
| 291eba63a7 | |||
| d98b0da410 | |||
| af55cdd8a6 | |||
| 0ec1beac8f | |||
| d541b92253 | |||
|
|
e279fc76ee |
@@ -0,0 +1,13 @@
|
||||
---
|
||||
# nuzlocke-tracker-eg7j
|
||||
title: Fix JWT verification failing in local dev (HS256 fallback)
|
||||
status: completed
|
||||
type: bug
|
||||
priority: normal
|
||||
created_at: 2026-03-22T08:37:18Z
|
||||
updated_at: 2026-03-22T08:38:57Z
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
## Summary of Changes\n\nAdded HS256 fallback to JWT verification so local GoTrue (which signs with HMAC) works alongside the JWKS/RS256 path used in production. Added `SUPABASE_JWT_SECRET` config setting, passed it in docker-compose.yml, and updated .env.example files.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"asyncpg==0.31.0",
|
||||
"alembic==1.18.4",
|
||||
"PyJWT==2.12.1",
|
||||
"cryptography==45.0.3",
|
||||
"cryptography==45.0.7",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -3,7 +3,7 @@ from uuid import UUID
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from jwt import PyJWKClient, PyJWKClientError
|
||||
from jwt import PyJWKClient, PyJWKClientError, PyJWKSetError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -44,26 +44,40 @@ 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:
|
||||
pass
|
||||
except PyJWKClientError:
|
||||
pass
|
||||
except PyJWKSetError:
|
||||
pass
|
||||
return _verify_jwt_hs256(token)
|
||||
|
||||
|
||||
def get_current_user(request: Request) -> AuthUser | None:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@supabase/supabase-js": "^2.99.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "5.91.3",
|
||||
"@tanstack/react-query": "5.94.5",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -1817,9 +1817,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.91.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.2.tgz",
|
||||
"integrity": "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw==",
|
||||
"version": "5.94.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.94.5.tgz",
|
||||
"integrity": "sha512-Vx1JJiBURW/wdNGP45afjrqn0LfxYwL7K/bSrQvNRtyLGF1bxQPgUXCpzscG29e+UeFOh9hz1KOVala0N+bZiA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -1827,12 +1827,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.91.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.91.3.tgz",
|
||||
"integrity": "sha512-D8jsCexxS5crZxAeiH6VlLHOUzmHOxeW5c11y8rZu0c34u/cy18hUKQXA/gn1Ila3ZIFzP+Pzv76YnliC0EtZQ==",
|
||||
"version": "5.94.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.94.5.tgz",
|
||||
"integrity": "sha512-1wmrxKFkor+q8l+ygdHmv0Sq5g84Q3p4xvuJ7AdSIAhQQ7udOt+ZSZ19g1Jea3mHqtlTslLGJsmC4vHFgP0P3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.91.2"
|
||||
"@tanstack/query-core": "5.94.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@supabase/supabase-js": "^2.99.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/react-query": "5.91.3",
|
||||
"@tanstack/react-query": "5.94.5",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
Reference in New Issue
Block a user