Auth

OAuth 2.1 Flows


Supabase Auth implements OAuth 2.1 with OpenID Connect (OIDC), supporting the authorization code flow with PKCE and refresh token flow. This guide explains how these flows work in detail.

Supported grant types

Supabase Auth supports two OAuth 2.1 grant types:

  1. Authorization Code with PKCE (authorization_code) - For obtaining initial access tokens
  2. Refresh Token (refresh_token) - For obtaining new access tokens without re-authentication

Authorization code flow with PKCE

The authorization code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for all OAuth clients, including single-page applications, mobile apps, and server-side applications.

How it works

The flow consists of several steps:

  1. Client initiates authorization - Third-party app redirects user to Supabase Auth's authorize endpoint
  2. Supabase validates and redirects - Supabase Auth validates OAuth parameters and redirects user to your configured authorization URL
  3. User authenticates and authorizes - Your frontend checks if user is logged in, shows consent screen, and handles approval/denial
  4. Authorization code issued - Supabase Auth generates a short-lived authorization code and redirects back to client
  5. Code exchange - Client exchanges the code for tokens
  6. Access granted - Client receives access token, refresh token, and ID token

Flow diagram

Here's a visual representation of the complete authorization code flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐│ │ │ │ │ ││ Client │ │ Your Auth UI │ │ Supabase Auth ││ App │ │ (Frontend) │ │ ││ │ │ │ │ │└──────┬──────┘ └────────┬─────────┘ └────────┬─────────┘ │ │ │ │ 1. Generate PKCE params │ │ (code_verifier, code_challenge) │ │ │ │ │ 2. Redirect to /oauth/authorize with code_challenge │ ├────────────────────────────────────────────────────────────────>│ │ │ │ │ │ 3. Validate params & redirect │ │ │ to authorization_path │ │ │<────────────────────────────────┤ │ │ │ │ │ 4. getAuthorizationDetails() │ │ ├────────────────────────────────>│ │ │ Return client info │ │ │<────────────────────────────────┤ │ │ │ │ │ 5. User login & consent │ │ │ │ │ │ 6. approveAuthorization() │ │ ├────────────────────────────────>│ │ │ Return redirect_to with code │ │ │<────────────────────────────────┤ │ │ │ │ 7. Redirect to client callback with code │ │<───────────────────────────────────────────────────────────────┤ │ │ │ │ 8. Exchange code for tokens (POST /oauth/token) │ │ with code_verifier │ ├────────────────────────────────────────────────────────────────>│ │ │ │ │ 9. Return tokens (access, refresh, ID) │ │<────────────────────────────────────────────────────────────────┤ │ │ │ │ 10. Access resources with access_token │ │ │ │ │ 11. Refresh tokens (POST /oauth/token with refresh_token) │ ├────────────────────────────────────────────────────────────────>│ │ │ │ │ 12. Return new tokens │ │<────────────────────────────────────────────────────────────────┤ │ │ │

Key points:

  • Third-party client redirects user to Supabase Auth's authorize endpoint (not directly to your UI)
  • Supabase Auth validates OAuth parameters and redirects to your authorization path
  • Your frontend UI handles authentication and consent using supabase-js OAuth methods
  • Supabase Auth handles all backend OAuth logic (code generation, token issuance)

Step 1: Generate PKCE parameters

Before initiating the flow, the client must generate PKCE parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Generate a random code verifier (43-128 characters)function generateCodeVerifier() { const array = new Uint8Array(32) crypto.getRandomValues(array) return base64URLEncode(array)}// Create code challenge from verifierasync function generateCodeChallenge(verifier) { const encoder = new TextEncoder() const data = encoder.encode(verifier) const hash = await crypto.subtle.digest('SHA-256', data) return base64URLEncode(new Uint8Array(hash))}function base64URLEncode(buffer) { return btoa(String.fromCharCode(...buffer)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '')}// Generate and store verifier (you'll need it later)const codeVerifier = generateCodeVerifier()sessionStorage.setItem('code_verifier', codeVerifier)// Generate challenge to send in authorization requestconst codeChallenge = await generateCodeChallenge(codeVerifier)

Step 2: Authorization request

The client redirects the user to your authorization endpoint with the following parameters:

1
2
3
4
5
6
7
https://<project-ref>.supabase.co/auth/v1/oauth/authorize? response_type=code &client_id=<client-id> &redirect_uri=<configured-redirect-uri> &state=<random-state> &code_challenge=<code-challenge> &code_challenge_method=S256

Required parameters

ParameterDescription
response_typeMust be code for authorization code flow
client_idThe client ID from registration
redirect_uriMust exactly match a registered redirect URI
code_challengeThe generated code challenge
code_challenge_methodMust be S256 (SHA-256)

Optional parameters

ParameterDescription
stateRandom string to prevent CSRF attacks (highly recommended)
scopeSpace-separated list of scopes (e.g., openid email profile phone). Requested scopes will be included in the access token and control what information is returned by the UserInfo endpoint. Default scope when none provided is email. If the openid scope is requested, an ID token will be included in the response.
nonceRandom string for replay attack protection. If provided, will be included in the ID token.

After receiving the authorization request, Supabase Auth validates the OAuth parameters (client_id, redirect_uri, PKCE, etc.) and then redirects the user to your configured authorization path (e.g., https://example.com/oauth/consent?authorization_id=<id>).

The URL will contain an authorization_id query parameter that identifies this authorization request.

Your frontend application at the authorization path should:

  1. Extract authorization_id - Get the authorization_id from the URL query parameters
  2. Fetch authorization details - Call supabase.auth.oauth.getAuthorizationDetails(authorization_id) to retrieve information about the OAuth client and request parameters
  3. Check user authentication - Verify if the user is logged in; if not, redirect to your login page (preserving the full authorization path including the authorization_id). After successful login, redirect the user back to the authorization path with the same authorization_id query parameter
  4. Display consent screen - Show the user information about the requesting client (name, redirect URI, scopes)
  5. Handle user decision - When the user approves or denies:
    • Call supabase.auth.oauth.approveAuthorization(authorization_id) to approve
    • Call supabase.auth.oauth.denyAuthorization(authorization_id) to deny
    • Redirect user to the returned redirect_to URL

This is a frontend implementation using supabase-js. Supabase Auth handles all the backend OAuth logic (generating authorization codes, validating requests, etc.) after you call the approve/deny methods.

See the Getting Started guide for complete implementation examples.

Step 4: Authorization code issued

If the user approves access, Supabase Auth redirects back to the client's redirect URI with an authorization code:

1
2
3
https://client-app.com/callback? code=<authorization-code> &state=<state-from-request>

The authorization code is:

  • Short-lived - Valid for 10 minutes
  • Single-use - Can only be exchanged once
  • Bound to PKCE - Can only be exchanged with the correct code verifier

If the user denies access, Supabase Auth redirects with error information in query parameters:

1
2
3
4
https://client-app.com/callback? error=access_denied &error_description=The+user+denied+the+authorization+request &state=<state-from-request>

The error parameters allow clients to display relevant error messages to users:

ParameterDescription
errorError code (e.g., access_denied, invalid_request, server_error)
error_descriptionHuman-readable error description explaining what went wrong
stateThe state parameter from the original request (for CSRF protection)

Step 5: Token exchange

The client exchanges the authorization code for tokens by making a POST request to the token endpoint:

1
2
3
4
5
6
7
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=authorization_code' \ -d 'code=<authorization-code>' \ -d 'client_id=<client-id>' \ -d 'redirect_uri=<redirect-uri>' \ -d 'code_verifier=<code-verifier>'

For confidential clients (with client secret):

1
2
3
4
5
6
7
8
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=authorization_code' \ -d 'code=<authorization-code>' \ -d 'client_id=<client-id>' \ -d 'client_secret=<client-secret>' \ -d 'redirect_uri=<redirect-uri>' \ -d 'code_verifier=<code-verifier>'

Example in JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Retrieve the code verifier from storageconst codeVerifier = sessionStorage.getItem('code_verifier')// Exchange code for tokensconst response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, client_id: '<client-id>', redirect_uri: '<redirect-uri>', code_verifier: codeVerifier, }),})const tokens = await response.json()

Step 6: Token response

On success, Supabase Auth returns a JSON response with tokens:

1
2
3
4
5
6
7
8
{ "access_token": "eyJhbGc...", "token_type": "bearer", "expires_in": 3600, "refresh_token": "MXff...", "scope": "openid email profile", "id_token": "eyJhbGc..."}
FieldDescription
access_tokenJWT access token for accessing resources
token_typeAlways bearer
expires_inToken lifetime in seconds (default: 3600)
refresh_tokenToken for obtaining new access tokens
scopeGranted scopes from the authorization request
id_tokenOpenID Connect ID token (included only if openid scope was requested in the authorization request)

Access token structure

Access tokens are JWTs containing standard Supabase claims plus OAuth-specific claims:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{ "aud": "authenticated", "exp": 1735819200, "iat": 1735815600, "iss": "https://<project-ref>.supabase.co/auth/v1", "sub": "user-uuid", "email": "user@example.com", "phone": "", "app_metadata": { "provider": "email", "providers": ["email"] }, "user_metadata": {}, "role": "authenticated", "aal": "aal1", "amr": [ { "method": "password", "timestamp": 1735815600 } ], "session_id": "session-uuid", "client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d"}

OAuth-specific claims

ClaimDescription
client_idThe OAuth client ID that obtained this token

All other claims follow the standard Supabase JWT structure.

Available scopes

The following scopes are currently supported:

ScopeDescription
openidEnables OpenID Connect. When requested, an ID token will be included in the response.
emailGrants access to email and email_verified claims
profileGrants access to profile information (name, picture, etc.)
phoneGrants access to phone_number and phone_number_verified claims

Default scope: When no scope is specified in the authorization request, the default scope is email.

Scopes affect what information is included in ID tokens and returned by the UserInfo endpoint. All OAuth access tokens have full access to user data (same as regular session tokens), with the addition of the client_id claim. Use Row Level Security policies with the client_id claim to control which data each OAuth client can access.

Refresh token flow

Refresh tokens allow clients to obtain new access tokens without requiring the user to re-authenticate.

When to refresh

Clients should refresh access tokens when:

  • The access token is expired (check the exp claim)
  • The access token is about to expire (proactive refresh)
  • An API call returns a 401 Unauthorized error

Refresh request

Make a POST request to the token endpoint with the refresh token:

1
2
3
4
5
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token' \ -d 'refresh_token=<refresh-token>' \ -d 'client_id=<client-id>'

For confidential clients:

1
2
3
4
5
6
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token' \ -d 'refresh_token=<refresh-token>' \ -d 'client_id=<client-id>' \ -d 'client_secret=<client-secret>'

Example in JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function refreshAccessToken(refreshToken) { const response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: '<client-id>', }), }) if (!response.ok) { throw new Error('Failed to refresh token') } return await response.json()}

Refresh response

The response contains a new access token and optionally a new refresh token:

1
2
3
4
5
6
7
{ "access_token": "eyJhbGc...", "token_type": "bearer", "expires_in": 3600, "refresh_token": "v1.MXff...", "scope": "openid email profile"}

OpenID Connect (OIDC)

Supabase Auth supports OpenID Connect, an identity layer on top of OAuth 2.1.

ID tokens

ID tokens are JWTs that contain user identity information. They are signed by Supabase Auth and can be verified by clients.

The claims included in the ID token depend on the scopes requested during authorization. For example, requesting openid email profile will include email and profile-related claims, while requesting only openid email will include only email-related claims.

Example ID token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ "iss": "https://<project-ref>.supabase.co/auth/v1", "sub": "user-uuid", "aud": "client-id", "exp": 1735819200, "iat": 1735815600, "auth_time": 1735815600, "nonce": "random-nonce-from-request", "email": "user@example.com", "email_verified": true, "phone_number": "+1234567890", "phone_number_verified": false, "name": "John Doe", "picture": "https://example.com/avatar.jpg"}

Standard OIDC claims

ClaimDescription
subSubject (user ID)
nonceThe nonce value from the authorization request (if provided)
emailUser's email address
email_verifiedWhether the email is verified
phone_numberUser's phone number
phone_number_verifiedWhether the phone is verified
nameUser's full name
pictureUser's profile picture URL

UserInfo endpoint

Clients can retrieve user information by calling the UserInfo endpoint with an access token:

1
2
curl 'https://<project-ref>.supabase.co/auth/v1/oauth/userinfo' \ -H 'Authorization: Bearer <access-token>'

The information returned depends on the scopes granted in the access token. For example:

With email scope:

1
2
3
4
5
{ "sub": "user-uuid", "email": "user@example.com", "email_verified": true}

With email profile phone scopes:

1
2
3
4
5
6
7
8
9
{ "sub": "user-uuid", "email": "user@example.com", "email_verified": true, "phone_number": "+1234567890", "phone_number_verified": false, "name": "John Doe", "picture": "https://example.com/avatar.jpg"}

OIDC discovery

Supabase Auth exposes OpenID Connect and OAuth 2.1 discovery endpoints that describe its capabilities:

1
2
https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configurationhttps://<project-ref>.supabase.co/auth/v1/.well-known/oauth-authorization-server

These endpoints return metadata about:

  • Available endpoints (authorization, token, userinfo, JWKS)
  • Supported grant types and response types
  • Supported scopes and claims
  • Token signing algorithms

This enables automatic integration with OIDC-compliant libraries and tools.

Token validation

Third-party clients should validate access tokens to ensure they're authentic and not tampered with.

JWKS endpoint

Supabase Auth exposes a JSON Web Key Set (JWKS) endpoint containing public keys for token verification:

1
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json

Example response:

1
2
3
4
5
6
7
8
9
10
11
12
{ "keys": [ { "kty": "RSA", "kid": "key-id", "use": "sig", "alg": "RS256", "n": "...", "e": "AQAB" } ]}

Validating tokens

Use a JWT library to verify tokens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createRemoteJWKSet, jwtVerify } from 'jose'const JWKS = createRemoteJWKSet( new URL('https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json'))async function verifyAccessToken(token) { try { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://<project-ref>.supabase.co/auth/v1', audience: 'authenticated', }) return payload } catch (error) { console.error('Token verification failed:', error) return null }}

What to validate

Always verify:

  1. Signature - Token is signed by Supabase Auth
  2. Issuer (iss) - Matches your project URL
  3. Audience (aud) - Is authenticated
  4. Expiration (exp) - Token is not expired
  5. Client ID (client_id) - Matches your client (if applicable)

Managing user grants

Users can view and manage the OAuth applications they've authorized to access their account. This is important for transparency and security, allowing users to audit and revoke access when needed.

Viewing authorized applications

Users can retrieve a list of all OAuth clients they've authorized:

1
2
3
4
5
6
7
const { data: grants, error } = await supabase.auth.oauth.getUserGrants()if (error) { console.error('Error fetching grants:', error)} else { console.log('Authorized applications:', grants)}

The response includes details about each authorized OAuth client:

1
2
3
4
5
6
7
8
9
10
[ { "id": "grant-uuid", "client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "client_name": "My Third-Party App", "scopes": ["email", "profile"], "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" }]

Revoking access

Users can revoke access for a specific OAuth client at any time. When access is revoked, all active sessions and refresh tokens for that client are immediately invalidated:

1
2
3
4
5
6
7
const { error } = await supabase.auth.oauth.revokeGrant(clientId)if (error) { console.error('Error revoking access:', error)} else { console.log('Access revoked successfully')}

After revoking access:

  • All refresh tokens for that client are deleted
  • The user will need to re-authorize the application to grant access again

For complete API reference, see the OAuth methods in supabase-js.

Next steps