Skip to content

PAT auth returns HTTP 500 (not 401) in Supabase deployment mode #286

@cgbarlow

Description

@cgbarlow

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions