Error handling
Where identity's errors appear, what each OAuth error means and what to do, how the SDK reports them, and what to log.
On this page
Most integration problems are visible in an error and invisible in a log. This page says where each kind of error arrives, what it means and what your code should do, so that your application fails clearly and your logs say why.
Where errors arrive#
| Where | What you get |
|---|---|
| The redirect back from sign-in | Query or form parameters: error, error_description, state, iss. |
| The pushed authorization, token, revocation and introspection endpoints | A JSON body { "error": "...", "error_description": "..." }, and a status. |
| The authorization endpoint, for a request that cannot be sent back to you | In a browser, a redirect to identity's own error page. For any other caller, the same JSON body with status 400. |
/userinfo | A status with a WWW-Authenticate header, and a JSON body. |
| Your own API | Whatever you return. See Protect your API. |
Statuses on the JSON endpoints: 400 for most errors, 401 for invalid_client at the token and revocation endpoints, 500 for server_error, and 429 for a rate limit. The pushed authorization endpoint answers invalid_client with 400, not 401. So read error, never the status alone.
The error codes#
error | Meaning | What to do |
|---|---|---|
invalid_request | A parameter is missing, malformed, repeated, or not one identity accepts. A redirect address that is not registered exactly is this too. The description names the parameter. | Fix the request. Do not retry. |
invalid_client | Client authentication failed: unknown or suspended client, no live key for the kid, a bad or reused assertion, or no DPoP key proved where one is required. | Check the key, the assertion and your clock. See Client authentication. |
invalid_grant | A code, refresh token or subject token is unknown, expired, spent, bound to another key, or its session has ended (this includes a session that was pinned to a single sign-on connection which is no longer linked to the organization). | Do not retry the same value. Send the person through sign-in again, or fetch a new service token. |
invalid_scope | A scope the client is not registered for, or a refresh asking for more than the family holds. | Ask for fewer scopes, or have the registration changed. |
invalid_target | A resource or audience that is malformed, unregistered or not allowed to you, or a resource where none is allowed. A resource whose product is not installed in the organization is refused the same way. | See Resource indicators. |
invalid_request_uri | The request_uri is unknown, expired or already used. | Start again with a new pushed request. |
unauthorized_client | The client does not hold the grant it used, or has no service identity. | Have the registration changed. |
unsupported_response_type, unsupported_grant_type | You asked for something identity does not offer. Only code and four grant types exist. | See Choosing an integration. |
access_denied | The person refused consent, or the account is not a member of the organization you asked for, or your product is not installed there. The answer is the same for every cause, on purpose. | An ordinary outcome. Show a page that explains it. |
login_required, consent_required, interaction_required | You sent prompt=none and the request needs a page. interaction_required is what an organization that enforces single sign-on answers. | Retry without prompt=none, or show your own sign-in button. |
insufficient_user_authentication | The organization enforces single sign-on (the sso_enforcement flag, which defaults OFF) and the person's session holds no recent enough assurance from one of its connections. Status 400 at the token endpoint, with acr_values and max_age in the body (RFC 9470). | Send the person through a new authorization that asks for that max_age. The SDK's rp.stepUpUrl does this. See Relying-party module. |
invalid_dpop_proof | The proof is malformed, for the wrong request or key, too old or too new, or reused. | See DPoP and Clock skew. |
use_dpop_nonce | The proof needs the server's current nonce, in the DPoP-Nonce header. | Sign a new proof with it and retry once. This is routine. |
temporarily_unavailable | Status 429. Too many requests. | Wait for Retry-After. See Rate limits and retries. |
server_error | Status 500. Identity failed. | Retry later with a back-off. |
The generated list of error codes is in Error codes. It does not include temporarily_unavailable, which is the 429 answer above.
From userinfo and resource servers#
Bearer endpoints use the WWW-Authenticate header.
| Status | error | Meaning |
|---|---|---|
| 401 | none, body {"error":"unauthorized"} | No credentials, or an unknown scheme. The header offers both Bearer and DPoP. |
| 401 | invalid_token | The token is unknown, expired, revoked, for the wrong audience, or its session ended. |
| 401 | use_dpop_nonce | Retry with the DPoP-Nonce header's value. |
| 403 | insufficient_scope | The token is valid and does not carry the scope the endpoint needs. |
| 400 | invalid_request | A malformed Authorization header, or an access token in the query string, which is refused. |
A resource server you build with the SDK answers every refusal with 401 and {"error":"invalid_token"} or {"error":"invalid_dpop_proof"}, and never names a claim. When you set requireAuthentication it also answers {"error":"insufficient_user_authentication"} with the acr_values and max_age the token fell short of, in the WWW-Authenticate challenge (RFC 9470). See Resource-server verification.
What the SDK reports#
The SDK throws typed errors. All extend one base class, so a single catch can handle the family.
| Class | Meaning | Useful fields |
|---|---|---|
ConfigurationError | Your code or configuration is wrong, as opposed to the network. | reason, such as invalid_options, invalid_key, insecure_url, endpoint_unavailable, alg_unsupported |
DiscoveryError | Discovery could not be read or does not match the issuer. | reason, such as unavailable, malformed, issuer_mismatch |
OAuthEndpointError | A pushed authorization, token or revocation request failed. | reason, status, oauthError, description, retryAfterSeconds, attempts |
AuthorizationResponseError | The redirect back is not acceptable: state or iss wrong, duplicated, transaction expired, or the server reported an error. | reason, oauthError, description |
IdTokenError | The ID token failed validation. | reason, such as nonce, signature, exp |
LogoutTokenError | A back-channel logout token failed validation. | reason, such as replay, events |
OAuthEndpointError.reason is one of oauth_error (the server returned an OAuth error body), rate_limited, server_error, malformed_response or transport (no response). Treat description as untrusted text from the network. A rate_limited error is what is left after the SDK's own bounded retry: attempts says how many requests it made. See Rate limits and retries.
The resource-server verifier and the decision client check their options when you create them and throw a plain RangeError for an algorithm other than EdDSA or ES256 and for a clockToleranceSeconds outside 0 to 300. The relying party and the service client report the same mistakes as ConfigurationError with reason invalid_options.
The route helpers turn a failed callback into a fixed answer: 400 with one of invalid_tenant (from the login route, when getTenant returns something that is not a tenant id), provider_error, missing_state, missing_transaction, invalid_transaction, state_mismatch, invalid_response, token_exchange_failed or token_verification_failed, and 500 for internal_error. The body repeats no text from identity. Pass onError to render your own page.
What to show the person#
Almost never the error. A person cannot act on invalid_grant. Show a short message that says sign-in did not finish, a button to try again, and a way to get help. Put the code in the log and, if you like, a request reference on the page.
What to log#
Log the error code, the endpoint, the client id, the status, Retry-After if present, the description, and a correlation id of your own. The error page identity shows in a browser carries a request reference. Quote it when you ask TDACorp for help.
Never log tokens, refresh tokens, authorization codes, client assertions, DPoP proofs or private keys. Treat every error_description as text from outside: log it, and do not render it as HTML or parse it.
Retry or not#
| Error | Retry? |
|---|---|
use_dpop_nonce | Once, at once, with a new proof and a new assertion. |
temporarily_unavailable, server_error, transport | With back-off, within limits. The SDK itself retries only a 429, a bounded number of times, and never a server_error or transport failure. |
| Everything else | No. The request or the registration needs to change. |