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).
{
"_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"
]
}
}
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:
| Field | Type | Required | Description |
|---|---|---|---|
authorize | boolean | No | true to grant, false (default) to deny |
response_type | string | Yes | Must be code |
client_id | string | Yes | The client's client_id |
redirect_uri | string | Yes | Must match a registered redirect URI |
scope | string | No | Space-delimited requested scopes |
state | string | No | CSRF token, echoed back on the redirect |
code_challenge | string | No | PKCE challenge (required for PKCE clients) |
code_challenge_method | string | No | PKCE 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": true,
"response_type": "code",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:profile read:dataset",
"state": "csrf-state-value"
}
""
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.
{
"authorize": false,
"response_type": "code",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"redirect_uri": "https://myapp.example.com/callback",
"scope": "read:profile",
"state": "csrf-state-value"
}
""
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):
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | authorization_code |
code | string | Yes | The single-use authorization code |
client_id | string | Yes | The client's client_id (or use HTTP Basic) |
client_secret | string | Conditional | A client secret (or use HTTP Basic); not required for PKCE clients |
redirect_uri | string | No | Must match the authorize request |
code_verifier | string | PKCE | Required 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.
{
"grant_type": "authorization_code",
"code": "0f1e2d3c4b5a69788796",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"client_secret": "example-client-secret-shown-once",
"redirect_uri": "https://myapp.example.com/callback"
}
{
"_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
}
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.
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | refresh_token |
refresh_token | string | Yes | The refresh token from a prior exchange |
client_id | string | Yes | The client's client_id (or use HTTP Basic) |
client_secret | string | Conditional | A client secret (or use HTTP Basic) |
{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature",
"client_id": "1a2b3c4d5e6f7a8b9c0d",
"client_secret": "example-client-secret-shown-once"
}
{
"_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
}
Each refresh issues a new refresh_token that differs from the one presented.
Replaying a spent refresh token answers 401 (replay detection).
Client credentials can be sent as payload fields (client_id, client_secret)
or via HTTP Basic authentication (Authorization: Basic base64(client_id:client_secret)).