OAuth 2.1 and OpenID Connect server
Tested with: supabase-js 2.117 · coreos/go-oidc 3 · golang.org/x/oauth2
Your project can act as an OAuth 2.1 authorization server and OpenID Connect provider for its own users. Other apps send your users to your project to sign in, your consent page asks them to approve, and the app receives tokens that act as that user, under your row-level security. AI agents and MCP clients use the same flow, and can register themselves.
The paths are Supabase Auth’s OAuth server paths, so supabase.auth.oauth.* (consent) and supabase.auth.admin.oauth.*
(clients) work unchanged.
How it works
Section titled “How it works”- The app redirects the user to
GET https://<ref>.us-east.databasezy.com:8443/auth/v1/oauth/authorizewithresponse_type=code, itsclient_id, an exactredirect_uri, ascope, astateand a PKCEcode_challenge(S256). - The project redirects the browser to your consent page with
?authorization_id=.... The page, where the user is signed in to your app, shows who is asking and for what, and approves or denies. - The browser goes back to the app’s
redirect_uriwithcode,stateandiss. - The app exchanges the code at
POST /auth/v1/oauth/tokenwith itscode_verifier(and its secret if it has one), and receives an access token, a refresh token and, withopenid, an ID token.
Turn it on
Section titled “Turn it on”-
Build the consent page in your app (any URL on your domain), with supabase-js:
app/oauth/consent/page.ts const id = new URLSearchParams(location.search).get("authorization_id")!;const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(id);if (data && "redirect_url" in data) {location.assign(data.redirect_url); // the user already granted these scopes} else if (data) {// Show data.client.name, data.client.logo_uri and data.scope; then on "Allow":await supabase.auth.oauth.approveAuthorization(id);// or on "Deny": await supabase.auth.oauth.denyAuthorization(id);}Clients that registered themselves (
client.registration_type === "dynamic") have not been checked by you: say so on the page. -
Enable the server in the portal (Platform → Auth → OAuth server) with the consent page URL, or with the API:
shell zb api PUT /v1/orgs/$ORG/projects/$PROJECT/auth/oauth-server \'{"enabled": true, "authorization_url": "https://app.example.com/oauth/consent"}' -
Register a client (portal: New client). A confidential client (a server) gets a secret, shown once; a public client (a single-page app, a mobile or desktop app, a CLI, an MCP client) has none and proves itself with PKCE.
Endpoints
Section titled “Endpoints”| Endpoint | Path (under /auth/v1) | Credentials |
|---|---|---|
| Discovery | /.well-known/openid-configuration, /.well-known/oauth-authorization-server (also /.well-known/oauth-authorization-server/auth/v1 at the host root) | none |
| JWKS | /.well-known/jwks.json | none |
| Authorization | GET /oauth/authorize | none (a browser) |
| Token | POST /oauth/token (authorization_code, refresh_token) | client secret (Basic or form) or PKCE only |
| Revocation (RFC 7009) | POST /oauth/revoke | the client’s |
| UserInfo | GET /oauth/userinfo | the access token |
| Dynamic registration (RFC 7591) | POST /oauth/clients/register | none, when allowed |
| Consent API | GET /oauth/authorizations/{id}, POST /oauth/authorizations/{id}/consent | the signed-in user |
| The user’s grants | GET /user/oauth/grants, DELETE /user/oauth/grants?client_id= | the signed-in user |
| Client management | /admin/oauth/clients[/{id}[/regenerate_secret]] | secret key |
The issuer is https://<ref>.us-east.databasezy.com:8443/auth/v1.
Tokens and scopes
Section titled “Tokens and scopes”- Access token: an ES256 JWT signed with the project’s key, like a session token:
roleauthenticated,sub,session_id,aal, plusclient_id,scopeand an audience that names the client (and theresourcethe client asked for, RFC 8707). It works with the Data API, Storage and Realtime under your RLS policies. Restrict what a client can do withauth.jwt() ->> 'client_id'in a policy. - ID token (scope
openid): audience = the client id, withnonce,at_hash,auth_timeand the claims of the granted scopes. It is not a credential: the project endpoint refuses it. - Scopes:
openid,email(email,email_verified),profile(name,picture,preferred_username, …),phone(phone_number,phone_number_verified). - Refresh tokens rotate on every use; reusing an old one ends the session. They refresh only at
/oauth/tokenwith the client they were issued to.
MCP clients
Section titled “MCP clients”An MCP client needs only the discovery URL. With Allow dynamic client registration on, it registers itself as a
public client, runs the authorization code flow with PKCE against your consent page and calls your MCP server (for
example a function) with the access token; the server verifies it with the project’s JWKS and
checks that aud contains its own URL.
Security
Section titled “Security”- Authorization code flow only (no implicit or password grant); PKCE
S256required for every client. - Redirect URIs are matched exactly: https,
http://localhost/127.0.0.1for native apps, or a reverse-domain custom scheme; no wildcards, no fragments. A wrong client or redirect URI is answered on the project host, never redirected. - Authorization codes are single use, valid two minutes and bound to the client, the redirect URI and the PKCE challenge; a replayed code ends the session the first exchange created.
- Client secrets are stored as SHA-256 digests and compared in constant time; failed client authentication is rate limited and repeated failures raise a security event.
- Consent is recorded per user, client and scope, and revocable by the user (
DELETE /user/oauth/grants); revoking ends the client’s sessions. Deleting a client ends all of them. - Dynamic registration is off by default; when on, it is limited to 10 registrations per IP per hour and 200 per project per day, at most 1,000 clients per project.
- A token issued to a client cannot approve consents for another client.