10 Commits

Author SHA1 Message Date
Renovate Bot
c896075ead chore(deps): update dependency cryptography to v45.0.7
Some checks failed
renovate/artifacts Artifact file update failure
CI / backend-tests (pull_request) Failing after 46s
CI / frontend-tests (pull_request) Successful in 33s
2026-03-22 09:02:05 +00:00
ac0a04e71f fix: catch PyJWKSetError in JWT verification fallback
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 28s
PyJWKSetError is not a subclass of PyJWKClientError — they are siblings
under PyJWTError. The empty JWKS key set error was not being caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:56:58 +01:00
94cc74c0fb Merge pull request 'Fix except clause syntax in JWT verification fallback' (#81) from feature/fix-except-clause-syntax-in-jwt-verification into develop
All checks were successful
CI / backend-tests (push) Successful in 30s
CI / frontend-tests (push) Successful in 28s
Reviewed-on: #81
2026-03-22 09:53:43 +01:00
41a18edb4f fix: use separate except clauses for JWT verification fallback
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 29s
ruff format strips parentheses from `except (A, B):`, turning it into
Python 2 comma syntax that only catches the first exception. Use
separate except clauses so PyJWKClientError is actually caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 09:52:33 +01:00
291eba63a7 chore: update bean 2026-03-22 09:42:15 +01:00
d98b0da410 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
2026-03-22 09:41:39 +01:00
af55cdd8a6 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>
2026-03-22 09:38:52 +01:00
0ec1beac8f Merge pull request 'Migrate JWT verification from HS256 to JWKS' (#75) from feature/migrate-jwt-verification-to-jwks into develop
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 28s
Reviewed-on: #75
2026-03-22 09:26:22 +01:00
d541b92253 Merge pull request 'chore(deps): update dependency @tanstack/react-query to v5.94.5' (#78) from renovate/tanstack-react-query-5.x into develop
Some checks failed
CI / frontend-tests (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
Reviewed-on: #78
2026-03-22 09:25:37 +01:00
Renovate Bot
e279fc76ee chore(deps): update dependency @tanstack/react-query to v5.94.5
All checks were successful
CI / backend-tests (pull_request) Successful in 27s
CI / frontend-tests (pull_request) Successful in 28s
2026-03-21 16:01:57 +00:00
10 changed files with 58 additions and 29 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
"@dnd-kit/utilities": "3.2.2", "@dnd-kit/utilities": "3.2.2",
"@supabase/supabase-js": "^2.99.3", "@supabase/supabase-js": "^2.99.3",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "5.91.3", "@tanstack/react-query": "5.94.5",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -1817,9 +1817,9 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.91.2", "version": "5.94.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.94.5.tgz",
"integrity": "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw==", "integrity": "sha512-Vx1JJiBURW/wdNGP45afjrqn0LfxYwL7K/bSrQvNRtyLGF1bxQPgUXCpzscG29e+UeFOh9hz1KOVala0N+bZiA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1827,12 +1827,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.91.3", "version": "5.94.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.91.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.94.5.tgz",
"integrity": "sha512-D8jsCexxS5crZxAeiH6VlLHOUzmHOxeW5c11y8rZu0c34u/cy18hUKQXA/gn1Ila3ZIFzP+Pzv76YnliC0EtZQ==", "integrity": "sha512-1wmrxKFkor+q8l+ygdHmv0Sq5g84Q3p4xvuJ7AdSIAhQQ7udOt+ZSZ19g1Jea3mHqtlTslLGJsmC4vHFgP0P3A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.91.2" "@tanstack/query-core": "5.94.5"
}, },
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -21,7 +21,7 @@
"@dnd-kit/utilities": "3.2.2", "@dnd-kit/utilities": "3.2.2",
"@supabase/supabase-js": "^2.99.3", "@supabase/supabase-js": "^2.99.3",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "5.91.3", "@tanstack/react-query": "5.94.5",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",