Skip to main content

Authorization Code Flow

The standard OAuth 2.0 authorization code grant (with optional PKCE), plus scope discovery and token exchange.

The flow is: discover scopes, redirect the user to authorize, receive an authorization code on the redirect, then exchange that code for tokens. The authorize step is gated by the token API license feature; the token exchange authenticates the client by its credentials rather than a user session.

GET /api/oauth/scopes

List the registered OAuth scopes a client may request.

Authentication: Required (no token API feature gate)

Response:

A HAL collection under _embedded["items"]. The registered set is read/write for dataset, profile, report, and global. Each scope's name and description are i18n message keys (resolved by the client to the active locale), and isDefault appears only on default scopes (it is not serialized as false on the others).

List OAuth scopesGET /api/oauth/scopes
GET /api/oauth/scopes is a HAL collection (_embedded.items) of the registered scope drivers. The real set is read/write for dataset, profile, report, and global. `name`/`description` are i18n message keys (resolved client-side), and `isDefault` appears only on default scopes (it is not serialized as false). Authenticated route; no `token.api` feature gate.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/scopes"
}
},
"start": 0,
"count": 8,
"total": 8,
"_embedded": {
"inf:oauth-scope": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/scopes/read%3Adataset"
}
},
"id": "read:dataset",
"name": "Datasets Read",
"description": "The application will have read access to Datasets and Dataset data.",
"isDefault": true
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/scopes/read%3Aglobal"
}
},
"id": "read:global",
"name": "Full Access Read",
"description": "The application will have full read access to your Informer account.",
"isDefault": false
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/scopes/read%3Aprofile"
}
},
"id": "read:profile",
"name": "Profile Read",
"description": "The application will have read access to your User profile information.",
"isDefault": true
},
"… 5 more items (8 total) — omitted from docs"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/oauth/authorize

The user consent step. With authorize: true, issues a single-use authorization code (60 second TTL, Redis-backed) and redirects to the client's registered redirect_uri.

Authentication: Signed-in user + token API feature

Request Body:

FieldTypeRequiredDescription
authorizebooleanNotrue to grant, false (default) to deny
response_typestringYesMust be code
client_idstringYesThe client's client_id
redirect_uristringYesMust match a registered redirect URI
scopestringNoSpace-delimited requested scopes
statestringNoCSRF token, echoed back on the redirect
code_challengestringNoPKCE challenge (required for PKCE clients)
code_challenge_methodstringNoPKCE method, plain or S256

Example Request:

{
"authorize": true,
"response_type": "code",
"client_id": "your-client-id",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:profile read:dataset",
"state": "csrf-state-value"
}

Response:

Answers 302 with an empty body. The authorization code (and state, when supplied) ride on the Location header's query string, not the response body.

Authorize and issue an authorization codePOST /api/oauth/authorize
POST /api/oauth/authorize with authorize:true issues a single-use authorization code (60s TTL) and answers 302, redirecting to the client redirect_uri with `code` (and `state` when supplied). The code is in the Location header — the response body is empty. Requires the `token.api` license feature. `state` is omitted when not supplied, so always pass a CSRF state. PKCE clients must additionally send code_challenge/code_challenge_method.
Request body
{
"authorize": true,
"response_type": "code",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:profile read:dataset",
"state": "csrf-state-value"
}
Response · 302
""
Captured from the API examples test suite; ids and timestamps are normalized.
Always send a state

state is omitted from the redirect when you do not supply it, so always pass a CSRF state and verify it on the callback.

When the user denies consent (authorize: false, the default), the route still answers 302 but redirects with error=access_denied instead of a code. Other pre-validation failures (invalid scope, unsupported response_type, missing PKCE parameters) redirect the same way with their own error code. The one non-redirect failure is an unregistered redirect_uri, which answers 400.

Authorization deniedPOST /api/oauth/authorize
When the user does not consent (authorize:false, the default), the route still answers 302 but redirects to the client redirect_uri with `error=access_denied` instead of a code. Other pre-validation failures (invalid scope, unsupported response_type, missing PKCE params) redirect the same way with their own `error` code. The one non-redirect failure is an unregistered redirect_uri, which 400s.
Request body
{
"authorize": false,
"response_type": "code",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:profile",
"state": "csrf-state-value"
}
Response · 302
""
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/oauth/access_token

Exchange an authorization code (or a refresh token) for an access token.

Authentication: Client credentials (client_id / client_secret in the payload, or HTTP Basic). There is no user session, so callers send the x-tenant header to identify the tenant.

Request Body (authorization_code grant):

FieldTypeRequiredDescription
grant_typestringYesauthorization_code
codestringYesThe single-use authorization code
client_idstringYesThe client's client_id (or use HTTP Basic)
client_secretstringConditionalA client secret (or use HTTP Basic); not required for PKCE clients
redirect_uristringNoMust match the authorize request
code_verifierstringPKCERequired when code_challenge was sent on authorize

Example Request:

{
"grant_type": "authorization_code",
"code": "the-authorization-code",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"redirect_uri": "https://myapp.example.com/callback"
}

Response:

Answers 200 (a plain object, not 201) with access_token, token_type, and scope. refresh_token and expires_in (900 seconds) are present only when the client has enableRefreshTokens. The tokens are JWTs.

Exchange an authorization code for tokensPOST /api/oauth/access_token
POST /api/oauth/access_token with grant_type=authorization_code returns { access_token, token_type:"bearer", scope } and answers 200 (a plain object, NOT 201). The client authenticates via client_id/client_secret in the payload (or HTTP Basic) — there is no user session, so callers send the `x-tenant` header. The authorization code is single-use; reusing it 401s. refresh_token + expires_in (900s) appear only when the client has enableRefreshTokens:true (it does here). Bad credentials 401; an invalid/expired code 401; a redirect_uri mismatch 401.
Request body
{
"grant_type": "authorization_code",
"code": "0f1e2d3c4b5a69788796",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"client_secret": "example-client-secret-shown-once",
"redirect_uri": "https://myapp.example.com/callback"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/access_token"
}
},
"access_token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature",
"token_type": "bearer",
"scope": "read:profile",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.example-token-2.signature",
"expires_in": 900
}
Captured from the API examples test suite; ids and timestamps are normalized.
Single-use code

The authorization code is single-use and expires after 60 seconds; reusing it, presenting bad client credentials, or sending a mismatched redirect_uri all answer 401.

Refresh token grant

Send grant_type=refresh_token with the stored refresh_token to obtain a fresh access token.

FieldTypeRequiredDescription
grant_typestringYesrefresh_token
refresh_tokenstringYesThe refresh token from a prior exchange
client_idstringYesThe client's client_id (or use HTTP Basic)
client_secretstringConditionalA client secret (or use HTTP Basic)
Refresh an access tokenPOST /api/oauth/access_token
POST /api/oauth/access_token with grant_type=refresh_token returns a fresh { access_token, token_type, scope, refresh_token, expires_in }. Refresh tokens ROTATE on use: the new refresh_token differs from the one presented, and replaying a spent refresh token 401s (replay detection). Same client-credential auth (payload fields or HTTP Basic) and `x-tenant` header as the code exchange.
Request body
{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"client_secret": "example-client-secret-shown-once"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/access_token"
}
},
"access_token": "eyJhbGciOiJIUzI1NiJ9.example-token-2.signature",
"token_type": "bearer",
"scope": "read:profile",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.example-token-3.signature",
"expires_in": 900
}
Captured from the API examples test suite; ids and timestamps are normalized.
Refresh tokens rotate

Each refresh issues a new refresh_token that differs from the one presented. Replaying a spent refresh token answers 401 (replay detection).

Client authentication

Client credentials can be sent as payload fields (client_id, client_secret) or via HTTP Basic authentication (Authorization: Basic base64(client_id:client_secret)).