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, plus the special mcp scope (never granted by default, and meaningful only alongside an RFC 8707 resource; see MCP Clients). 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": 9,
"total": 9,
"_embedded": {
"inf:oauth-scope": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/oauth/scopes/mcp"
}
},
"id": "mcp",
"name": "App Tools (MCP)",
"description": "The application can list and call the authorized app's MCP tools, acting as you.",
"isDefault": false
},
{
"_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
},
"… 6 more items (9 total) — omitted from docs"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /oauth/authorize​

The browser entry to the consent flow, and the URL the authorization server metadata advertises as authorization_endpoint. It lives at the server root, not under /api, and it is a page, not an API: send the user's browser here, not an HTTP client.

Authentication: optional (mode: try). An unauthenticated visitor is redirected to /go/login?redirectTo=<this URL> (with Cache-Control: no-store) and returns here after signing in. An authenticated visitor falls through to the single-page app, which renders the consent screen. The consent screen then submits POST /api/oauth/authorize.


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
scopestringYesSpace-delimited requested scopes
statestringNoCSRF token, echoed back on the redirect
code_challengestringNoPKCE challenge (required for PKCE clients)
code_challenge_methodstringNoPKCE method, plain or S256
resourcestringNoRFC 8707 resource indicator: the absolute URL of an App's MCP endpoint (/api/apps/{id}/mcp on this server). Binds the issued code, and the token exchanged for it, to that App. A resource on another origin, or one naming an App the consenting user cannot read, redirects with error=invalid_target.

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, an unresolvable resource as invalid_target, or an internal fault as server_error) redirect the same way with their own error code. The one non-redirect failure is an unregistered redirect_uri, which answers 400. A scope outside the client's allowedScopes cap (dynamically registered clients are capped at mcp) also redirects with error=invalid_scope, even if the user consents.

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
resourcestringNoMust match the resource sent on authorize. A different value answers 400 with invalid_target; omitting it keeps the binding the code already carries.

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.

Failures answer the RFC 6749 error shape, {"error", "error_description"}, with error drawn from the spec's code set (invalid_grant, invalid_client, invalid_request, unsupported_grant_type, ...). Every failure is 400 except bad client credentials, which answer 401 (invalid_client) with a WWW-Authenticate: Basic header.

Single-use code

The authorization code is single-use and expires after 60 seconds. Reusing it or sending a mismatched redirect_uri answers 400 (invalid_grant); bad client credentials answer 401 (invalid_client).

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 400 with error: "invalid_grant" (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)).