OAuth Flow Endpoints
These are the OAuth 2.0 protocol endpoints clients use to obtain access tokens and revoke them. They implement RFC 6749 (OAuth 2.0 Authorization Framework), RFC 7009 (Token Revocation), RFC 7636 (PKCE), and RFC 8707 (Resource Indicators).
All endpoints are mounted on the public blueprint and require no bearer token. The locations are also advertised by the OAuth Discovery endpoints so clients can configure themselves automatically.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET, POST | /oauth/authorize | Begin an authorization code flow |
| POST | /oauth/token | Exchange a code, refresh token, or credentials for an access token |
| POST | /oauth/revoke | Revoke a refresh token |
Authorization Endpoint
GET /public/v2/oauth/authorize
POST /public/v2/oauth/authorize
Browser-redirect endpoint that initiates the authorization code grant. The user is asked to log in (if not already authenticated via an OAuth session cookie) and then to grant or deny consent. On success the user-agent is redirected to the client’s redirect_uri with an authorization code query parameter.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_id | string | Yes | Client identifier issued during client registration |
| redirect_uri | string | Yes | Redirect URI registered for this client. Must exactly match a registered URI |
| response_type | string | Yes | Must be code. Any other value redirects with error=unsupported_response_type |
| scope | string | No | Space-separated list of requested scopes |
| state | string | No | Opaque value echoed back on redirect so the client can correlate the request and defend against CSRF |
| resource | string | No | RFC 8707 resource indicator. If supplied, the same value must be sent to the token endpoint and will be bound as the access token’s audience |
| code_challenge | string | No | PKCE code challenge derived from the client’s code_verifier |
| code_challenge_method | string | No | PKCE method used to derive code_challenge. plain (default) or S256 |
Form Parameters (POST)
When the user submits the consent form, the endpoint accepts:
| Field | Type | Description |
|---|---|---|
| confirm | string | yes to issue an authorization code, anything else to deny |
Successful Response
The user-agent is redirected to:
{redirect_uri}?code={authorization_code}&state={state}
state is included only when the original request supplied one. The code is single-use and short-lived.
Denial Response
If the user denies access, the user-agent is redirected to:
{redirect_uri}?error=access_denied&state={state}
Error Responses
| Status | Response | Description |
|---|---|---|
| 400 | {"error": "invalid_request"} |
Missing client_id or redirect_uri |
| 400 | {"error": "invalid_client"} |
Unknown client_id |
| 400 | {"error": "invalid_redirect_uri"} |
The supplied redirect_uri is not registered for this client |
| 302 | Redirect with error=unsupported_response_type |
response_type is not code |
Login Flow
If the user is not authenticated when the endpoint is reached, the response is a 302 to /public/v2/oauth/login with the original authorize URL preserved in the next query parameter. After successful login the user-agent is sent back to the authorize endpoint and the consent form is shown.
Token Endpoint
POST /public/v2/oauth/token
Returns a bearer access token. Supports three grant types — authorization_code, refresh_token, and client_credentials. The request body is application/x-www-form-urlencoded.
Client Authentication
The client may authenticate either by including client_id and client_secret in the form body (client_secret_post) or by HTTP Basic auth (client_secret_basic). Both methods are advertised by the discovery endpoint.
Common Form Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | One of authorization_code, refresh_token, client_credentials |
| client_id | string | Conditional | Required when not using HTTP Basic auth |
| client_secret | string | Conditional | Required when not using HTTP Basic auth |
The grant type must be listed in the client’s registered grant_types; otherwise the response is 400 {"error": "unauthorized_client"}.
Grant: authorization_code
Exchanges a single-use authorization code obtained from the authorize endpoint for an access token and a refresh token.
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | The authorization code received on the redirect URI |
| redirect_uri | string | Yes | Must exactly match the redirect_uri sent to the authorize endpoint |
| code_verifier | string | Conditional | Required when the authorization request included a code_challenge |
| resource | string | Conditional | Required when the authorization request included a resource parameter, and must equal that value |
Response
{
"access_token": "<access_token>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<refresh_token>",
"scope": "read:accounting"
}
Grant: refresh_token
Exchanges a refresh token for a new access token. Refresh tokens are rotated — the old token is revoked and a new one is returned.
| Field | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | Yes | A valid, unrevoked refresh token issued to this client |
| scope | string | No | A subset of the originally granted scopes. Defaults to the original scope when omitted |
| resource | string | No | RFC 8707 resource indicator to bind as the audience of the new access token |
Response
Same shape as the authorization_code response. The refresh_token field contains the rotated refresh token — clients must store it and discard the previous one.
Grant: client_credentials
Server-to-server flow. The token is associated with the client’s owning user.
| Field | Type | Required | Description |
|---|---|---|---|
| scope | string | No | Space-separated list of requested scopes |
| resource | string | No | RFC 8707 resource indicator to bind as the audience of the access token |
Response
{
"access_token": "<access_token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:accounting"
}
No refresh_token is issued for the client credentials grant — clients re-request a token as needed.
Error Responses
| Status | Body | Description |
|---|---|---|
| 401 | {"error": "invalid_client"} |
Missing or incorrect client credentials |
| 400 | {"error": "unauthorized_client"} |
The grant type is not enabled for this client |
| 400 | {"error": "unsupported_grant_type"} |
The grant_type value is not recognised |
| 400 | {"error": "invalid_grant", "error_description": "Authorization code is required"} |
code missing on an authorization_code request |
| 400 | {"error": "invalid_grant", "error_description": "Invalid authorization code"} |
Code not found |
| 400 | {"error": "invalid_grant", "error_description": "Authorization code expired"} |
Code TTL elapsed |
| 400 | {"error": "invalid_grant", "error_description": "Authorization code was issued to another client"} |
Cross-client code reuse attempt |
| 400 | {"error": "invalid_grant", "error_description": "Redirect URI mismatch"} |
redirect_uri does not match the original authorize request |
| 400 | {"error": "invalid_grant", "error_description": "Code verifier is required"} |
PKCE expected but code_verifier not supplied |
| 400 | {"error": "invalid_grant", "error_description": "Code verifier is invalid"} |
PKCE verifier does not match the original challenge |
| 400 | {"error": "invalid_grant", "error_description": "Resource parameter is required"} |
resource was sent at authorize but missing at token exchange |
| 400 | {"error": "invalid_grant", "error_description": "Resource parameter mismatch"} |
resource value differs from the authorize request |
| 400 | {"error": "invalid_grant", "error_description": "Refresh token is required"} |
refresh_token missing |
| 400 | {"error": "invalid_grant", "error_description": "Invalid refresh token"} |
Refresh token not found, expired, or revoked |
| 400 | {"error": "invalid_grant", "error_description": "Refresh token was issued to another client"} |
Cross-client refresh attempt |
Revocation Endpoint
POST /public/v2/oauth/revoke
Revokes a token per RFC 7009. Only refresh tokens are durably revocable — access tokens are short-lived JWTs and are not tracked server-side, so revoking one returns success without effect.
The request body is application/x-www-form-urlencoded.
Form Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | The token to revoke |
| token_type_hint | string | No | refresh_token or access_token. The server still looks the token up if the hint is wrong, per RFC 7009 |
| client_id | string | Conditional | Required when not using HTTP Basic auth |
| client_secret | string | Conditional | Required when not using HTTP Basic auth |
Response
Returns 200 OK with an empty JSON body on success, including when the token is unknown — the spec requires that revocation requests for invalid tokens still succeed so that clients cannot probe for token existence.
{}
A refresh token is only actually revoked when both:
- The token is recognised.
- The token belongs to the authenticated client.
If those conditions are not met the response is still 200 {}.
Error Responses
| Status | Body | Description |
|---|---|---|
| 400 | {"error": "invalid_request"} |
token field is missing |
| 401 | {"error": "invalid_client"} |
Missing or incorrect client credentials |
Related Resources
- OAuth Discovery — well-known metadata endpoints that advertise these URLs
- OAuth Clients — register and manage OAuth client applications
- OAuth Admin — admin endpoints for listing and revoking tokens across users