diff --git a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx index 95ecc5a595e..b65a671d198 100644 --- a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx +++ b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx @@ -491,6 +491,22 @@ Store the client secret securely. It will only be shown once. If you lose it, yo +#### Token endpoint authentication method + +When a client exchanges an authorization code or refreshes a token, it must authenticate with the token endpoint. The `token_endpoint_auth_method` controls how this authentication happens: + +| Method | Description | Used by | +| --- | --- | --- | +| `none` | No client authentication. Only `client_id` is sent in the request body. | Public clients (required) | +| `client_secret_basic` | Client credentials sent via HTTP Basic auth (`Authorization: Basic `). **This is the default for confidential clients.** | Confidential clients | +| `client_secret_post` | Client credentials sent in the request body (`client_id` and `client_secret` as form parameters). | Confidential clients | + +**Defaults:** Public clients default to `none`. Confidential clients default to `client_secret_basic` (per [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591#section-2)). + +**Constraints:** Public clients must use `none`. Confidential clients cannot use `none`. + +You can set this when registering a client via the dashboard or programmatically. See [OAuth Flows](/docs/guides/auth/oauth-server/oauth-flows#step-5-token-exchange) for examples of each method in action. + @@ -518,6 +534,8 @@ const { data, error } = await supabase.auth.admin.oauth.createClient({ name: 'My Third-Party App', redirect_uris: ['https://my-app.com/auth/callback', 'https://my-app.com/auth/silent-callback'], client_type: 'confidential', + // Optional: defaults to 'client_secret_basic' for confidential, 'none' for public + token_endpoint_auth_method: 'client_secret_basic', }) if (error) { @@ -548,7 +566,9 @@ response = supabase.auth.admin.oauth.create_client({ 'https://my-app.com/auth/callback', 'https://my-app.com/auth/silent-callback' ], - 'client_type': 'confidential' + 'client_type': 'confidential', + # Optional: defaults to 'client_secret_basic' for confidential, 'none' for public + 'token_endpoint_auth_method': 'client_secret_basic' }) print('Client created:', response) @@ -572,7 +592,8 @@ curl -X POST 'https://.supabase.co/auth/v1/admin/oauth/clients' \ "https://my-app.com/auth/callback", "https://my-app.com/auth/silent-callback" ], - "client_type": "confidential" + "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_basic" }' ``` @@ -585,7 +606,8 @@ curl -X POST 'http://localhost:54321/auth/v1/admin/oauth/clients' \ -d '{ "name": "Local Dev App", "redirect_uris": ["http://localhost:3000/auth/callback"], - "client_type": "confidential" + "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_post" }' ``` @@ -598,6 +620,7 @@ Response: "name": "My Third-Party App", "redirect_uris": ["https://my-app.com/auth/callback", "https://my-app.com/auth/silent-callback"], "client_type": "confidential", + "token_endpoint_auth_method": "client_secret_basic", "created_at": "2025-01-15T10:30:00.000Z" } ``` diff --git a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx index 028634e88c7..ec39a958843 100644 --- a/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx +++ b/apps/docs/content/guides/auth/oauth-server/oauth-flows.mdx @@ -230,7 +230,11 @@ The error parameters allow clients to display relevant error messages to users: ### Step 5: Token exchange -The client exchanges the authorization code for tokens by making a POST request to the token endpoint: +The client exchanges the authorization code for tokens by making a POST request to the token endpoint. How the client authenticates depends on its `token_endpoint_auth_method` (set during [client registration](/docs/guides/auth/oauth-server/getting-started#token-endpoint-authentication-method)). + +#### Public clients (`token_endpoint_auth_method: none`) + +Public clients send only the `client_id` in the request body with no secret: ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -242,7 +246,29 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ -d 'code_verifier=' ``` -For confidential clients (with client secret): +#### Confidential clients (`token_endpoint_auth_method: client_secret_basic`) + +This is the **default** for confidential clients. Credentials are sent via the `Authorization` header using HTTP Basic authentication (base64-encoded `client_id:client_secret`): + +```bash +curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -u ':' \ + -d 'grant_type=authorization_code' \ + -d 'code=' \ + -d 'redirect_uri=' \ + -d 'code_verifier=' +``` + + + +The `-u` flag in cURL automatically encodes the credentials and sets the `Authorization: Basic ` header. If you're not using cURL, you must base64-encode the `client_id:client_secret` string yourself. + + + +#### Confidential clients (`token_endpoint_auth_method: client_secret_post`) + +Credentials are sent as form parameters in the request body: ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -261,7 +287,7 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ // Retrieve the code verifier from storage const codeVerifier = sessionStorage.getItem('code_verifier') -// Exchange code for tokens +// --- Public clients (token_endpoint_auth_method: none) --- const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { method: 'POST', headers: { @@ -276,6 +302,37 @@ const response = await fetch(`https://.supabase.co/auth/v1/oauth/to }), }) +// --- Confidential clients (token_endpoint_auth_method: client_secret_basic) --- +const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Basic ' + btoa(':'), + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: '', + code_verifier: codeVerifier, + }), +}) + +// --- Confidential clients (token_endpoint_auth_method: client_secret_post) --- +const response = await fetch(`https://.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_secret: '', + redirect_uri: '', + code_verifier: codeVerifier, + }), +}) + const tokens = await response.json() ``` @@ -377,7 +434,9 @@ Clients should refresh access tokens when: ### Refresh request -Make a POST request to the token endpoint with the refresh token: +Make a POST request to the token endpoint with the refresh token. The client authenticates the same way as during the [token exchange](#step-5-token-exchange), based on its `token_endpoint_auth_method`. + +#### Public clients (`token_endpoint_auth_method: none`) ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -387,7 +446,17 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ -d 'client_id=' ``` -For confidential clients: +#### Confidential clients (`token_endpoint_auth_method: client_secret_basic`) + +```bash +curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -u ':' \ + -d 'grant_type=refresh_token' \ + -d 'refresh_token=' +``` + +#### Confidential clients (`token_endpoint_auth_method: client_secret_post`) ```bash curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ @@ -401,6 +470,7 @@ curl -X POST 'https://.supabase.co/auth/v1/oauth/token' \ #### Example in JavaScript ```javascript +// Public clients (token_endpoint_auth_method: none) async function refreshAccessToken(refreshToken) { const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { method: 'POST', @@ -420,6 +490,27 @@ async function refreshAccessToken(refreshToken) { return await response.json() } + +// Confidential clients (token_endpoint_auth_method: client_secret_basic) +async function refreshAccessTokenConfidential(refreshToken) { + const response = await fetch(`https://.supabase.co/auth/v1/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Basic ' + btoa(':'), + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), + }) + + if (!response.ok) { + throw new Error('Failed to refresh token') + } + + return await response.json() +} ``` ### Refresh response