Errors
Every error the OAuth endpoints return, where it lands, and what to do about it.
Errors
Errors follow RFC 6749:
{
"error": "invalid_grant",
"error_description": "Invalid PKCE verifier"
}Where the error lands
This is the part most integrators get wrong, so it is worth stating plainly:
- Before
redirect_uriis proven registered — the error is returned to your app as JSON, with an HTTP status. SSO will not redirect, because redirecting to an unverified URI is exactly the open-redirect bug. - After that — the browser is redirected back to
redirect_uriwitherroranderror_descriptionappended, plus the samestateyou sent.
So handle both: a JSON body on the fetch, and error parameters on the callback.
There is one more place failures show up: the token endpoint never redirects. It always answers your backend directly.
Error reference
| Error | Status | Raised at | Usual cause |
|---|---|---|---|
invalid_request | 400 | authorize, consent-info, approve, token | Missing parameter; unknown client; unregistered redirect_uri; PKCE missing or not S256 |
unsupported_response_type | — (redirect) | authorize | response_type is anything other than code |
invalid_scope | 400 / redirect | authorize, consent-info, approve | A scope the client isn't registered for, or one SSO doesn't know |
access_denied | 401 | consent-info, approve, userinfo | No signed-in user |
invalid_client | 401 | token, revoke | Wrong or missing client credentials |
invalid_grant | 400 | token | Bad/expired/used code; PKCE mismatch; redirect_uri mismatch; invalid, expired, reused, or foreign refresh token |
unsupported_grant_type | 400 | token | grant_type other than authorization_code / refresh_token |
too_many_requests | 429 | token, revoke | More than 60 requests per 15 minutes from one IP |
server_error | 500 | any | Something broke — check the API logs |
The invalid_grant cases, separated
Because the code is the same for several very different problems:
| Cause | Detail |
|---|---|
| Code unknown | Never issued, or belongs to a different client |
| Code expired | Codes live 10 minutes |
| Code already used | Single-use. A replay also revokes that client's sessions for the user |
redirect_uri mismatch | Must be byte-identical to the authorize request |
| PKCE verifier wrong | Must hash to the original code_challenge |
| Refresh token unknown/expired | Never issued, revoked, or past 30 days |
| Refresh token reused | Rotated-out token presented → the whole session family is revoked |
| Refresh token not yours | clientId claim doesn't match the authenticating client, or the token came from a first-party session |
Diagnosing
Start at discovery — it tells you what the server is actually serving:
curl -s https://sso.aetherlabs.agency/api/oauth/.well-known/oauth-authorization-serverCheck that the URLs are https:// (if they read http://, the deployment's
PUBLIC_API_URL / trust-proxy setting is wrong), that
response_types_supported is ["code"], and that
code_challenge_methods_supported is ["S256"].
Then, in order:
invalid_requestat authorize → is theredirect_uriexactly registered?invalid_scope→ is every requested scope in the client's registered set?- Redirect back with
error=access_denied→ the user wasn't signed in, or declined. invalid_clientat token → the secret is wrong, rotated, or sent in neither Basic nor the body.invalid_grantat token → work down the table above; reusing a code or refresh token is the most common.
Common mistakes
- Reusing an authorization code, or a refresh token after rotation.
- Sending
scopevalues the client was never registered for. - Registering a callback URI that differs from the request by a slash, port, or query string.
- Treating
expires_inas 3600 — it is the real lifetime (86400). - Putting the
client_secretin frontend code. - Expecting the browser to redirect on an invalid
redirect_uri— it won't; that call returns JSON.