feat(docs): token_endpoint_auth_method in OAuth server docs (#43128)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Docs update

## Summary
The OAuth server supports three token endpoint authentication methods
(`none`, `client_secret_basic`, `client_secret_post`), but the docs only
showed `client_secret_post` implicitly without labeling it, and never
mentioned client_secret_basic (the actual default for confidential
clients per RFC 7591).

- Add `token_endpoint_auth_method` explanation with defaults/constraints
to the client registration section in getting-started.mdx
- Update registration examples (JS, Python, cURL) and response JSON to
include token_endpoint_auth_method
- Restructure token exchange and refresh token sections in
oauth-flows.mdx to show all three auth methods with clear labels
- Add `client_secret_basic` examples using HTTP Basic auth header
This commit is contained in:
Cemal Kılıç authored and GitHub committed 2026-02-24 17:29:47 +05:30
1 parent 85e6b1143f
commit d810b7772b
2 files changed
+122 -8

No files matched your search

@@ -491,6 +491,22 @@ Store the client secret securely. It will only be shown once. If you lose it, yo
</Admonition>
#### 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 <base64(client_id:client_secret)>`). **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.
</TabPanel>
<TabPanel id="programmatically" label="Programmatically">
@@ -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://<project-ref>.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"
}
```
@@ -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://<project-ref>.supabase.co/auth/v1/oauth/token' \
@@ -242,7 +246,29 @@ curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-d 'code_verifier=<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://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u '<client-id>:<client-secret>' \
-d 'grant_type=authorization_code' \
-d 'code=<authorization-code>' \
-d 'redirect_uri=<redirect-uri>' \
-d 'code_verifier=<code-verifier>'
```
<Admonition type="note">
The `-u` flag in cURL automatically encodes the credentials and sets the `Authorization: Basic <base64(client_id:client_secret)>` header. If you're not using cURL, you must base64-encode the `client_id:client_secret` string yourself.
</Admonition>
#### Confidential clients (`token_endpoint_auth_method: client_secret_post`)
Credentials are sent as form parameters in the request body:
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
@@ -261,7 +287,7 @@ curl -X POST 'https://<project-ref>.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://<project-ref>.supabase.co/auth/v1/oauth/token`, {
method: 'POST',
headers: {
@@ -276,6 +302,37 @@ const response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/to
}),
})
// --- Confidential clients (token_endpoint_auth_method: client_secret_basic) ---
const response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + btoa('<client-id>:<client-secret>'),
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: '<redirect-uri>',
code_verifier: codeVerifier,
}),
})
// --- Confidential clients (token_endpoint_auth_method: client_secret_post) ---
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: 'authorization_code',
code: authorizationCode,
client_id: '<client-id>',
client_secret: '<client-secret>',
redirect_uri: '<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://<project-ref>.supabase.co/auth/v1/oauth/token' \
@@ -387,7 +446,17 @@ curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-d 'client_id=<client-id>'
```
For confidential clients:
#### Confidential clients (`token_endpoint_auth_method: client_secret_basic`)
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u '<client-id>:<client-secret>' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=<refresh-token>'
```
#### Confidential clients (`token_endpoint_auth_method: client_secret_post`)
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
@@ -401,6 +470,7 @@ curl -X POST 'https://<project-ref>.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://<project-ref>.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://<project-ref>.supabase.co/auth/v1/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + btoa('<client-id>:<client-secret>'),
},
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