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.
Updated
On this page
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.

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

- 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-configurationand…/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.)
audis"authenticated"(the Supabase role), not your MCP URL; validateiss+ 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:
- Build the
/authorizeconsent page withsupabase-jsat your configured authorization path; that’s the one piece this guide stubs. - Run the four-curl self-test from the connector checklist, remembering every well-known URL lives under
/auth/v1. - 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.
Put it into practice
Supabase Auth OAuth Server for an MCP connector
Enable Supabase's OAuth Server beta, force the DCR toggle through the Management API, and validate the ES256 tokens in your MCP server, plus a fully headless CI proof.
Preview prompt
Configure my Supabase project (ref MY_REF [fill in]) as the OAuth 2.1 authorization server for my remote MCP server at https://MY_DOMAIN/mcp, using the OAuth Server beta in Supabase Auth.
Enablement:
- Dashboard: Authentication → Configuration → OAuth Server (BETA) → enable. Set the Authorization Path to /authorize and the Site URL (Auth → URL Configuration) to https://MY_DOMAIN; Supabase will redirect end users to https://MY_DOMAIN/authorize, where my app renders login + consent with supabase-js and approves the authorization.
- Dynamic client registration: the dashboard's "Allow Dynamic OAuth Apps" toggle may fail to persist (beta bug). Set it through the Management API instead, with a personal access token from Account → Access Tokens:
PATCH https://api.supabase.com/v1/projects/MY_REF/config/auth with {"oauth_server_allow_dynamic_registration": true}, then GET the config back and confirm it stuck.
Discovery facts my metadata must respect:
- The issuer is https://MY_REF.supabase.co/auth/v1, under /auth/v1, NOT the project root. authorization_servers in my protected-resource metadata must be exactly that.
- Both /auth/v1/.well-known/openid-configuration and /auth/v1/.well-known/oauth-authorization-server are served; registration_endpoint is /auth/v1/oauth/clients/register; S256 is supported.
Resource server behaviour (write or adapt my MCP server):
- /.well-known/oauth-protected-resource with resource = https://MY_DOMAIN/mcp and the /auth/v1 issuer as authorization server.
- 401 + WWW-Authenticate Bearer challenge (resource_metadata pointer) when unauthenticated; 401 on invalid tokens.
- Validate JWTs against https://MY_REF.supabase.co/auth/v1/.well-known/jwks.json. Supabase signs with ES256 (P-256), so my verifier must import EC keys, not just RSA. Check iss and exp; do NOT require aud to equal the MCP URL (Supabase sets aud to "authenticated").
Verification (run and show output):
- POST /auth/v1/oauth/clients/register with redirect_uris ["https://claude.ai/api/mcp/auth_callback"], token_endpoint_auth_method "none", grant_types ["authorization_code","refresh_token"] → 201 with a uuid client_id and "registration_type": "dynamic"; no null fields. Registered apps show under Authentication → OAuth Apps; there is no RFC 7592 management URI, so clean probes up in the dashboard.
- Headless CI proof without any browser: PATCH mailer_autoconfirm true via the Management API, create a test user, POST /auth/v1/token?grant_type=password with the anon key → the returned access_token is the same ES256 JWT the OAuth server issues; call my MCP endpoint with it and confirm a garbage token gets 401.
Secrets (access token, service_role, anon key) live in environment variables only, never in code or logs.Use this site from your agent
Add this URL as a connector in Claude, ChatGPT, or Grok. No sign-in required.
https://quickoauth.com/mcp