Summary
In Supabase deployment mode, PAT (Personal Access Token) authentication returns HTTP 500 for any iris_pat_-prefixed bearer token — including obviously-invalid ones that should cleanly return 401. This makes PAT auth completely unusable against Supabase deployments (CLI, MCP, CI/agents), and means iris login --token and POST /api/users/me/tokens-minted PATs can't be used at all.
By contrast, the JWT path works correctly: a malformed non-PAT bearer returns a clean 401, and anonymous reads return 200. So the crash is isolated to the PAT branch.
Environment
- Deployment: Supabase mode (PostgreSQL / asyncpg)
- Backend host observed:
https://iris-api-gtb3.onrender.com (iris-uat)
Reproduction
B=https://iris-api-gtb3.onrender.com
# 1) Well-formed but fake PAT → returns 500 (should be 401)
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer iris_pat_deadbeef_thisIsNotARealSecretValue123456" \
"$B/api/auth/me"
# → 500 "Internal Server Error"
# 2) Garbage NON-PAT bearer → 401 (correct; JWT path)
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer not-a-pat-just-garbage" "$B/api/auth/me"
# → 401 {"detail":"Invalid token"}
# 3) Anonymous read → 200 (control)
curl -s -o /dev/null -w "%{http_code}\n" "$B/api/diagrams/<some-public-diagram-id>"
# → 200
A real, freshly-minted PAT shows the same 500 on every endpoint (/api/auth/me, /api/users/me/tokens, /api/diagrams/{id}), so it is not specific to a particular token value.
Expected vs actual
| Case |
Expected |
Actual |
| Invalid/unknown PAT |
401 |
500 |
| Valid PAT |
200 |
500 (presumed — couldn't get past 500) |
| Invalid non-PAT JWT |
401 |
401 ✓ |
| Anonymous read |
200 |
200 ✓ |
Likely cause
backend/app/auth/dependencies.py routes iris_pat_-prefixed bearers to _get_current_user_pat → verify_pat (backend/app/tokens/service.py:82). verify_pat is written to return None (→ 401) on malformed/unknown/mismatched tokens and only catches VerifyMismatchError. A 500 means something on that path is raising an unhandled exception rather than returning None.
Candidates to check under Supabase/asyncpg (vs the SQLite path it appears modelled on):
- The
SELECT … FROM personal_access_tokens query / _normalize_row / datetime param conversion in the asyncpg adapter (backend/app/db/adapter.py).
- Whether the
personal_access_tokens table + indexes (migration m041_personal_access_tokens) are actually present in the Supabase database.
- The Argon2
hasher.verify(...) call receiving an unexpected type/None for token_hash.
Whatever the root cause, the path should also be hardened so a bad PAT yields 401, never 500 — matching the JWT branch.
Impact
iris login --token <pat> and all PAT-authenticated CLI / MCP / CI usage are unusable against Supabase deployments.
- Downstream: the
woolies-shopper skill's SKU cache writeback can't authenticate via PAT and has to fall back to a short-lived Supabase session JWT.
Filed from investigation while building the woolies-shopper skill; happy to provide more detail or test a fix.
Summary
In Supabase deployment mode, PAT (Personal Access Token) authentication returns HTTP 500 for any
iris_pat_-prefixed bearer token — including obviously-invalid ones that should cleanly return 401. This makes PAT auth completely unusable against Supabase deployments (CLI, MCP, CI/agents), and meansiris login --tokenandPOST /api/users/me/tokens-minted PATs can't be used at all.By contrast, the JWT path works correctly: a malformed non-PAT bearer returns a clean 401, and anonymous reads return 200. So the crash is isolated to the PAT branch.
Environment
https://iris-api-gtb3.onrender.com(iris-uat)Reproduction
A real, freshly-minted PAT shows the same 500 on every endpoint (
/api/auth/me,/api/users/me/tokens,/api/diagrams/{id}), so it is not specific to a particular token value.Expected vs actual
401500200500(presumed — couldn't get past 500)401401✓200200✓Likely cause
backend/app/auth/dependencies.pyroutesiris_pat_-prefixed bearers to_get_current_user_pat→verify_pat(backend/app/tokens/service.py:82).verify_patis written to returnNone(→ 401) on malformed/unknown/mismatched tokens and only catchesVerifyMismatchError. A 500 means something on that path is raising an unhandled exception rather than returningNone.Candidates to check under Supabase/asyncpg (vs the SQLite path it appears modelled on):
SELECT … FROM personal_access_tokensquery /_normalize_row/ datetime param conversion in the asyncpg adapter (backend/app/db/adapter.py).personal_access_tokenstable + indexes (migrationm041_personal_access_tokens) are actually present in the Supabase database.hasher.verify(...)call receiving an unexpected type/None fortoken_hash.Whatever the root cause, the path should also be hardened so a bad PAT yields 401, never 500 — matching the JWT branch.
Impact
iris login --token <pat>and all PAT-authenticated CLI / MCP / CI usage are unusable against Supabase deployments.woolies-shopperskill's SKU cache writeback can't authenticate via PAT and has to fall back to a short-lived Supabase session JWT.Filed from investigation while building the woolies-shopper skill; happy to provide more detail or test a fix.