quickoauth
← Guides

Add OAuth to your MCP server with Supabase Auth

Supabase's OAuth Server beta as an MCP authorization server: the /auth/v1 issuer, ES256 keys, dynamic client registration, and the dashboard toggle that only the Management API can flip. Executed live against a real project.

supabase · updated 2026-08-22


Supabase Auth grew an OAuth Server feature (beta) that makes your project an OAuth 2.1 authorization server — if you already keep users in Supabase, your MCP connector can log in with them directly. We enabled it on a fresh project, hit one real dashboard bug (with the workaround), and validated an ES256 Supabase JWT inside a Worker MCP server. Testbed: https://qo-test-supabase.rough-disk-9b56.workers.dev/mcp.

1. Project setup

Sign-up is free (GitHub/ChatGPT OAuth or email+password — the email path is behind an hCaptcha). Create a project; the free plan is plenty.

New project form

2. Enable the OAuth server

Authentication → Configuration → OAuth Server (BETA) — toggle it on. The page then reveals the endpoints:

OAuth Server settings

  • Authorization endpoint: https://<ref>.supabase.co/auth/v1/oauth/authorize
  • Token endpoint: https://<ref>.supabase.co/auth/v1/oauth/token
  • JWKS: https://<ref>.supabase.co/auth/v1/.well-known/jwks.json
  • Discovery: …/auth/v1/.well-known/openid-configuration and …/auth/v1/.well-known/oauth-authorization-server

The Authorization Path is a route in your app (we used /authorize) where you render login + consent with supabase-js — Supabase redirects there, your page calls the approve API. Set your Site URL (Auth → URL Configuration) to the app that hosts it.

3. The issuer lives under /auth/v1

The issuer is https://<ref>.supabase.co/auth/v1 — not the project root. Your protected-resource metadata must advertise exactly that in authorization_servers, and clients will fetch /auth/v1/.well-known/oauth-authorization-server relative to it. Both RFC 8414 and OIDC discovery documents are served; S256 is supported; registration_endpoint appears once DCR is on.

4. Dynamic client registration — and the toggle that wouldn’t stick

The “Allow Dynamic OAuth Apps” switch in the dashboard refused to persist for us (beta UI bug: click, save, still off). The Management API sets the same field reliably:

curl -X PATCH https://api.supabase.com/v1/projects/$REF/config/auth \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"oauth_server_allow_dynamic_registration": true}'

($SUPABASE_ACCESS_TOKEN is a personal access token from Account → Access Tokens.) Then registration works exactly the way Claude does it:

curl -X POST https://$REF.supabase.co/auth/v1/oauth/clients/register \
  -H 'content-type: application/json' -d '{
  "client_name": "probe",
  "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"]
}'
# → 201, uuid client_id, "registration_type": "dynamic", no null fields

Registered apps appear under Authentication → OAuth Apps. There’s no RFC 7592 management URI in the response, so probe clients accumulate until you delete them in the dashboard.

5. Validate the tokens: ES256, aud is not your URL

Two things break naive JWT middleware here:

  • Supabase signs with ES256 (P-256), not RS256 — make sure your verifier imports EC keys from the JWKS. (Projects still on the legacy shared-secret HS256 setup must migrate to JWT signing keys first.)
  • aud is "authenticated" (the Supabase role), not your MCP URL — validate iss + signature + exp, not audience-as-resource.

The rest is the standard resource-server contract: PRM document, 401 with a WWW-Authenticate challenge, 401 for garbage tokens.

6. Headless proof for CI

You never need a browser to prove the chain: enable mailer_autoconfirm (Management API), create a user, and use the classic password grant with your project’s anon key:

curl -X POST "https://$REF.supabase.co/auth/v1/token?grant_type=password" \
  -H "apikey: $ANON_KEY" -H 'content-type: application/json' \
  -d '{"email":"demo@example.com","password":"…"}'

The access_token that returns is the same ES256 JWT the OAuth server issues; send it to your MCP endpoint. Our RL suite replays discovery, DCR, token mint, and the authenticated MCP call against this project on every deploy — 92 live checks across five deployments at the time of writing.

Next steps

To take this from testbed to your real connector:

  1. Build the /authorize consent page with supabase-js at your configured authorization path — that’s the one piece this guide stubs.
  2. Run the four-curl self-test from the connector checklist, remembering every well-known URL lives under /auth/v1.
  3. Connect in claude.ai under Customize → Connectors; then widen the allowlist for ChatGPT and Grok with one server, three connectors.

The Supabase OAuth prompt (supabase-oauth-mcp) hands the whole sequence to your agent, Management-API workaround included. If a step fails, use debugging connector failures. To compare providers, see Auth0 and Stytch, or go fully self-hosted with Hydra.

Metadata

Commit
b5cb085
Browser
Current Time
Dimensions
Source
guides
Last Updated
2026-08-22