Compare commits
7 Commits
0ec1beac8f
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c896075ead | ||
| ac0a04e71f | |||
| 94cc74c0fb | |||
| 41a18edb4f | |||
| 291eba63a7 | |||
| d98b0da410 | |||
| af55cdd8a6 |
@@ -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
|
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
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ dependencies = [
|
|||||||
"asyncpg==0.31.0",
|
"asyncpg==0.31.0",
|
||||||
"alembic==1.18.4",
|
"alembic==1.18.4",
|
||||||
"PyJWT==2.12.1",
|
"PyJWT==2.12.1",
|
||||||
"cryptography==45.0.3",
|
"cryptography==45.0.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import Depends, HTTPException, Request, status
|
from fastapi import Depends, HTTPException, Request, status
|
||||||
from jwt import PyJWKClient, PyJWKClientError
|
from jwt import PyJWKClient, PyJWKClientError, PyJWKSetError
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -44,26 +44,40 @@ 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.ExpiredSignatureError:
|
|
||||||
return None
|
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return None
|
pass
|
||||||
except PyJWKClientError:
|
except PyJWKClientError:
|
||||||
return None
|
pass
|
||||||
|
except PyJWKSetError:
|
||||||
|
pass
|
||||||
|
return _verify_jwt_hs256(token)
|
||||||
|
|
||||||
|
|
||||||
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