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, so 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
c6eb99c
Browser
Current Time
Dimensions
Source
guides
Last Updated
2026-08-22