Error reference
An OAuth error body has error and error_description. The first table lists every error value the server defines, generated from the code. The tables after it list messages by endpoint, with their causes. We triggered each one on a test environment unless the section says we read it in the code.
Error codes
This table is generated from the OAuthError constants class and the status mapping in TokenExceptionHandler. A call site can override the default status, and the "Status set at a call site" column lists each override.
| Error code | Defined by | Default HTTP status | Status set at a call site | Constant referenced by |
|---|---|---|---|---|
invalid_request | RFC 6749 Section 5.2 | 400 | none | AuthorizationCodeGrantHandler, AuthorizeService, BackchannelAuthorizeController, CallerAuthenticator, CibaInitiationService, ClientAssertionValidator, ClientCredentialsGrantHandler, GrantValidation, OAuth2CallbackService, RefreshTokenGrantHandler, TokenController, TokenExchangeGrantHandler, TokenService |
invalid_client | RFC 6749 Section 5.2 | 401 | none | AuthorizeService, BackchannelAuthorizeController, CallerAuthenticator, ClientAssertionValidator, ClientIdMetadataService, GrantValidation, SpiffeAuthenticator, TokenExceptionHandler, TokenService |
invalid_grant | RFC 6749 Section 5.2 | 400 | none | AuthorizationCodeGrantHandler, BackchannelAuthService, RefreshTokenGrantHandler, TokenExchangeGrantHandler, TokenService |
unauthorized_client | RFC 6749 Section 5.2 | 400 | none | BackchannelAuthorizeController, GrantValidation |
unsupported_grant_type | RFC 6749 Section 5.2 | 400 | none | TokenController |
invalid_scope | RFC 6749 Section 5.2 | 400 | none | RefreshTokenGrantHandler |
access_denied | RFC 6749 Section 5.2 | 400 | 403 in CibaInitiationService | AuthorizeService, CibaInitiationService, ObligationOrchestrator, TokenService |
server_error | RFC 6749 Section 5.2 | 400 | 500 in CibaInitiationService | AuthorizeService, CibaInitiationService, ClientIdMetadataService, OAuth2CallbackService, ObligationOrchestrator |
invalid_token | RFC 6750 Section 3.1 | 401 | none | CallerAuthenticator, TokenExceptionHandler |
insufficient_scope | RFC 6750 Section 3.1 | 401 | none | CallerAuthenticator, TokenExceptionHandler |
invalid_authorization_details | RFC 9396 (RAR) | 400 | none | AuthorizeService, TokenController, TokenExceptionHandler |
invalid_target | RFC 8693 (Token Exchange) | 400 | none | AuthorizationCodeGrantHandler, ClientCredentialsGrantHandler, RefreshTokenGrantHandler, TokenExchangeGrantHandler, TokenService |
use_dpop_nonce | RFC 9449 (DPoP) | 400 | none | TokenController |
invalid_dpop_proof | RFC 9449 (DPoP) | 400 | none | no class references the constant |
approval_required | OpenID CIBA Core 1.0 | 400 | none | TokenExceptionHandler |
authorization_pending | OpenID CIBA Core 1.0 | 400 | none | TokenService |
slow_down | OpenID CIBA Core 1.0 | 400 | none | TokenService |
expired_token | OpenID CIBA Core 1.0 | 400 | none | TokenService |
invalid_binding_message | OpenID CIBA Core 1.0 | 400 | none | CibaInitiationService |
temporarily_unavailable | OpenID CIBA Core 1.0 | 400 | 503 in CibaInitiationService | CibaInitiationService |
invalid_dpop_proof is defined but never sent. An invalid DPoP proof leads to access_denied when the client must use DPoP, and to a Bearer token when it need not. See DPoP.
/token
| Response | Cause |
|---|---|
400 invalid_request, scope is required | No scope and no authorization_details |
400 invalid_request, resource must not include a fragment | resource has a # |
400 invalid_target, Resource could not be resolved to a known type | resource maps to no registered type |
400 unsupported_grant_type, Grant type not supported: implicit | Any grant other than authorization_code, client_credentials, refresh_token, token exchange and CIBA, such as password or implicit |
401 invalid_client, client_secret does not match the registered secret | Wrong secret |
401 invalid_client, client_secret required — client is registered as confidential | A confidential client sent only its client_id |
400 access_denied, Access denied | Policy refused. The /token audit row carries the reason, such as grant_type_not_allowed, dpop_proof_required, client_denied or spiffe_no_path_grants. |
400 access_denied, Access denied | A token exchange asked for more than the subject token was granted. The audit reason is token_exchange_actions_exceeded |
invalid_grant, Subject token issuer is not trusted | An ID token exchange whose issuer is not in trusted_issuers. Read in the code. |
400 invalid_grant, Invalid or expired actor token | A token exchange sent an actor token the server did not issue |
400 invalid_request, Unsupported subject_token_type: ... | A token exchange subject type other than access_token or id_token |
401 invalid_client, JWT-SVID aud claim does not contain expected audience: ... | A SPIFFE SVID minted for another audience |
400 approval_required | Policy wants a person to approve. We read this in the code. See CIBA approval. |
The authorization code grant can also return invalid_grant for an unknown, expired, used or mismatched code, or a wrong code_verifier. We read these in the code.
CIBA polling at /token
Every answer below is HTTP 400. We read them in the code. The integration test test_ciba.py checks each one.
error | Cause |
|---|---|
authorization_pending | No decision yet |
slow_down | Polled inside the interval |
access_denied | The approver refused |
expired_token | No decision within expires_in |
invalid_grant | Unknown auth_req_id, another client's, or already used. We saw Unknown auth_req_id. |
/bc-authorize
| Response | Cause |
|---|---|
400 invalid_request, Policy does not require approval for this request; request the token directly | Policy grants the request without approval |
400 invalid_request, Exactly one of login_hint, id_token_hint or login_hint_token is required | No hint was sent |
400 invalid_request | A hint that is not login_hint or does not name the approver, no binding_message, or more than one distinct approval needed |
400 invalid_binding_message | The message differs from the one policy shows the approver |
400 unauthorized_client | A public client |
401 invalid_client | Missing or wrong credentials |
403 access_denied, Policy denies this request | Policy refuses outright |
503 temporarily_unavailable | The approval IdP did not accept the notification |
500 server_error | Approval is misconfigured on the server |
We triggered the first two rows and the invalid_client row. We read the others in the code.
/authorize
These come back as a JSON body, not a redirect.
| Response | Cause |
|---|---|
400 invalid_request, response_type must be 'code' | Any other response_type, such as token |
400 invalid_request, PKCE code_challenge_method 'plain' is not allowed; use S256 | A plain challenge |
400 invalid_request, redirect_uri not registered for client | A redirect URI the client did not register. Read in the code. |
401 invalid_client, Unknown client | An unknown client_id that is not a metadata document URL. Read in the code. |
Some errors go to the redirect URI instead, as error and error_description query parameters. A public client with no code_challenge gets error=invalid_request, which we read in the code. A request with no session and no identity provider to send the user to got error=login_required.
/register
Every refusal is HTTP 400.
error and description | Cause |
|---|---|
invalid_redirect_uri | A redirect URI that is not https, loopback http or a private-use scheme |
invalid_client_metadata, Registration denied: no_overlapping_grant_types | No requested grant type is allowed for registration without a statement |
invalid_client_metadata, Registration denied: auth_method_not_allowed | The auth method is not in allowed_auth_methods |
invalid_client_metadata, Registration denied: missing_software_statement | A statement is required and none was sent |
invalid_client_metadata, Registration denied: missing_explicit_declarations:grant_types | The statement has no grant_types |
invalid_software_statement, The software statement could not be parsed or verified. | Unsigned, tampered or unverifiable |
invalid_software_statement, Registration denied: untrusted_issuer | The issuer is not in trusted_issuers |
unapproved_software_statement, Registration denied: denylist:software_id:<id> | The software ID is denylisted. An issuer gives denylist:issuer:<issuer>. |
unapproved_software_statement, Registration denied: invalid_resource_type | The statement names a type missing from the registry |
registration_requires_approval | The statement has "trust_level": "low" |
A request with no redirect_uris gets a generic HTTP 400 Bad Request body instead of an OAuth error.
/introspect
| Response | Cause |
|---|---|
200 {"active": false, "inactive_reason": "invalid_token"} | The token is unknown, expired or revoked |
200 {"active": false, "inactive_reason": "insufficient_scope"} | Policy refused the call described by context |
401 invalid_client, client_id or Bearer token is required | No caller credentials |
401 invalid_client, Client authentication failed | Unknown client or wrong secret |
401 invalid_client, Client is not entitled to introspection or uma_protection scope | The caller lacks the scope |
/revoke
/revoke answers HTTP 200 with an empty body for a known or an unknown token. A wrong client secret is the exception, with HTTP 401 invalid_client. A 200 does not mean the token was revoked. A token issued to another confidential client stays active, and a request with no client credentials revokes only a token issued to a public client. See Revocation.
Admin API
| Response | Cause |
|---|---|
401 {"error": "unauthorized", "error_description": "Valid admin API key required"} | Missing or wrong X-Admin-API-Key |
409 {"error": "Client with client_id '<id>' already exists"} | Creating a client that already exists |
422 {"error": "Policy data failed schema validation", "failures": [...]} | A control row that breaks its schema |
Send feedback about this page