What we're enabling
Supabase Auth ships with a full OAuth 2.1 server that supports exactly the pieces the MCP authorization spec requires:
- OAuth 2.1 with PKCE for the authorization code flow
- RFC 8414 authorization server metadata at a well-known URI
- RFC 7591 dynamic client registration — so Claude can self-register without you pre-creating a client
- JWT access tokens that include a
client_idclaim and survive normal RLS - Refresh tokens with rotation
This is recent enough (2026 GA) that most existing MCP+Supabase tutorials predate it and recommend hand-rolled auth instead. We're using the supported, on-spec path.
1. Enable the OAuth server in the dashboard
Supabase project → Authentication → Sign in / Providers → OAuth 2.1 server (left sidebar, "Authorization" section).
Toggle Enable OAuth 2.1 server on. The page will reveal:
- The base URL for the auth server:
https://<ref>.supabase.co/auth/v1 - A switch labelled Dynamic Client Registration — turn it on
- A switch labelled Client ID Metadata Documents — leave off for now; we're using DCR
2. Configure allowed redirect URIs
Still on the OAuth 2.1 server page, find Allowed redirect URIs and add at minimum:
http://127.0.0.1/*— local Claude Code uses ephemeral localhost ports during the flowhttp://localhost/*— same, different name- (Optional)
claude://callback— Claude Desktop's custom-scheme redirect
These are the URIs the authorization code can be returned to. Anything not on this list will be rejected (per OAuth 2.1's strict redirect URI matching rules).
3. Confirm the well-known endpoints
Hit them with curl — they're public:
curl https://<ref>.supabase.co/.well-known/oauth-authorization-server/auth/v1 | jqYou should see something like:
{
"issuer": "https://<ref>.supabase.co/auth/v1",
"authorization_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/authorize",
"token_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/token",
"registration_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/register",
"jwks_uri": "https://<ref>.supabase.co/auth/v1/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
"scopes_supported": ["openid", "email", "profile"]
}Three things to notice:
registration_endpoint— Claude will POST a small JSON document here to register itself.code_challenge_methods_supported: ["S256"]— PKCE is supported, MCP requires it.token_endpoint_auth_methods_supported: ["none", ...]—nonemeans public clients (no client secret) work, which is what MCP clients are.
If any of these are missing, double-check the dashboard toggles in step 1.
4. Also fetch the JWKS
curl https://<ref>.supabase.co/auth/v1/.well-known/jwks.json | jqReturns a JWKS document with a single RSA public key. We'll use this in step 6 to verify Supabase-issued JWTs in the Edge Function. Copy the kid and alg for sanity — they should be RS256.
5. Choose how users sign in
Supabase OAuth's job is "obtain an access token after the user authenticates somehow." The "somehow" is whatever Supabase Auth providers you have configured. Common choices:
- Magic link (no provider config needed; works out of the box)
- Email + password (toggle on in Authentication → Sign-in Method)
- Google / GitHub / etc. (configure each provider with its credentials)
For a team blueprint, email + password or a social provider like Google is friendliest. Magic link is fine for solo testing but a hassle if you want to log in repeatedly during development.
Enable one or more under Authentication → Sign-in Method in the dashboard.
6. Configure access-token claims
By default, Supabase JWTs include sub (the user ID), email, role: "authenticated", and a few standard OIDC claims. For RLS that's all we need — auth.uid() reads sub.
If you want richer claims (workspace IDs baked into the token, custom roles, etc.), you can add an access-token hook. For this blueprint we don't need one — RLS reads workspace membership from the database on every query, which is the correct level of indirection.
7. Sanity check the issuer URL — it matters
The MCP server (our Edge Function) is going to validate tokens by checking the iss claim. Supabase issues tokens with iss = https://<ref>.supabase.co/auth/v1. Note: with a trailing path component, no trailing slash.
In step 6 we'll hard-code https://<ref>.supabase.co/auth/v1 as the expected issuer and reject anything else. Worth confirming now by signing in once and inspecting the token:
# Get a token via the password grant (requires Email auth enabled)
curl -X POST "https://<ref>.supabase.co/auth/v1/token?grant_type=password" \
-H "apikey: <your-anon-key>" \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"your-password"}'Take the access_token, paste it into jwt.io, and verify the iss claim matches what you expect.
The auth server is alive. Step 5 builds the MCP server skeleton on top of Hono and implements the Protected Resource Metadata document Claude will use to find this auth server.