Authorization Code Flow
Standard OAuth 2.0 authorization code grant with optional PKCE support, plus scope discovery and token exchange.
OAuth Flow Overview
- Client redirects user to
POST /oauth/authorizewithclient_id,redirect_uri,scope, andresponse_type=code - User authorizes the application (sets
authorize: true) - Server issues authorization code and redirects to
redirect_uriwith the code - Client exchanges code via
POST /oauth/access_tokenwith the authorization code and client credentials - Server issues tokens - access token (and optionally refresh token)
GET /api/oauth/scopes
List all available OAuth scopes.
Authentication: Required
Response:
{
"_embedded": {
"items": [
{
"id": "read:dataset",
"name": "Read Datasets",
"description": "Read-only access to datasets",
"isDefault": true
},
{
"id": "write:dataset",
"name": "Write Datasets",
"description": "Read/write access to datasets",
"isDefault": true
},
{
"id": "read:global",
"name": "Read Global",
"description": "Read-only access to any endpoint"
},
{
"id": "write:global",
"name": "Write Global",
"description": "Full access to any endpoint"
}
]
}
}
| Field | Description |
|---|---|
isDefault | If true, this scope is requested by default when no scopes are specified |
POST /api/oauth/authorize
Process an authorization request. If the user authorizes, generates an authorization code and redirects to the client's redirect URI.
Authentication: Required
Payload:
{
"authorize": true,
"response_type": "code",
"client_id": "a6bd8f0f72b2c3275ff6",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:dataset write:dataset read:profile",
"state": "random-csrf-token",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256"
}
| Field | Type | Required | Description |
|---|---|---|---|
authorize | boolean | No | Whether the user authorizes the request (default: false) |
response_type | string | No | Must be "code" (default: "code") |
client_id | string | Yes | OAuth client ID |
redirect_uri | string | Yes | Must match a registered redirect URI |
scope | string | Yes | Space-separated list of scopes |
state | string | No | Opaque value for CSRF protection (returned unchanged) |
code_challenge | string | PKCE | Required for PKCE-enabled clients |
code_challenge_method | string | No | "plain" or "S256" (default: "plain") |
proxy | string | No | JWT for proxy-based redirect flows |
Success Response: 302 Redirect
https://myapp.example.com/callback?code=AUTH_CODE&state=random-csrf-token
The authorization code expires after 60 seconds and is single-use.
Error Redirects:
If the user denies or validation fails, the redirect includes error parameters:
| Error | Description |
|---|---|
access_denied | User did not authorize |
invalid_request | PKCE parameters missing for PKCE client |
invalid_scope | One or more requested scopes are invalid |
unsupported_response_type | response_type is not "code" |
server_error | Unexpected server error |
https://myapp.example.com/callback?error=access_denied&error_description=User+denied+access&state=random-csrf-token
POST /api/oauth/access_token
Exchange an authorization code or refresh token for access tokens.
Authentication: Via client credentials (Basic auth or payload fields)
Authorization Code Grant
Payload:
{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://myapp.example.com/callback",
"client_id": "a6bd8f0f72b2c3275ff6",
"client_secret": "cs_a1b2c3d4...",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be "authorization_code" |
code | string | Yes | Authorization code from the authorize step |
redirect_uri | string | No | Must match the URI used in the authorize request |
client_id | string | Yes | OAuth client ID (or via Basic auth) |
client_secret | string | Conditional | Required unless PKCE is used (or via Basic auth) |
code_verifier | string | PKCE | Required if code_challenge was provided during authorization |
Refresh Token Grant
Payload:
{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"client_id": "a6bd8f0f72b2c3275ff6",
"client_secret": "cs_a1b2c3d4..."
}
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be "refresh_token" |
refresh_token | string | Yes | Previously issued refresh token |
client_id | string | Yes | OAuth client ID |
client_secret | string | Conditional | Required unless PKCE client |
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"scope": "read:dataset write:dataset read:profile",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 900
}
| Field | Description |
|---|---|
access_token | JWT for authenticating API requests |
token_type | Always "bearer" |
scope | Granted scopes (space-separated) |
refresh_token | Only present if the client has enableRefreshTokens: true |
expires_in | Token lifetime in seconds (900 = 15 minutes, only when refresh tokens are enabled) |
Client credentials can be provided either as payload fields (client_id, client_secret) or via HTTP Basic authentication (Authorization: Basic base64(client_id:client_secret)).
Refresh tokens are rotated on each use. The previous refresh token is invalidated. If a previously-used refresh token is presented, it indicates a potential replay attack and the server will reject it.
Error Responses:
400 Bad Request- Invalid grant, expired code, or invalid PKCE verifier401 Unauthorized- Invalid client credentials