# invalid_grant

The authorization code, refresh token or device code is invalid, expired, already used, revoked, or not this client's (RFC 6749 §5.2).

## Meaning

Sent by `/token` only. For an authorization code: unknown or expired (codes are single
use and live for `issuer.auth_code_ttl`, 60 seconds by default), already redeemed,
issued to another client, redeemed with another `redirect_uri`, a `code_verifier` that
does not match, PKCE required but the code has no challenge, or a verifier for a code
without one. For a refresh token: unknown or expired, already rotated, another client's,
its session ended, every grant of the account retired, the account suspended or blocked,
or its grant family revoked. A device code issued to another client. A code, refresh
token or device code bound to a DPoP key, presented without a proof of that key.
Refusals of one kind share one description, so a stolen credential cannot be told from
an expired one.

## Impact

For a refresh, the user is signed out of the application, which must send them through
`/authorize` again. For a code, the sign-in the user just completed fails at the last
step. Redeeming a code twice or presenting a used refresh token also revokes the whole
grant family: its refresh token stops working and its access tokens are refused at the
server's own endpoints. Two concurrent refreshes with one token, or a retried refresh
whose response was lost, cause exactly this.

## Diagnosis

1. Look the request up by `X-Request-ID` on the dashboard's Troubleshooting page. The
   refusal is recorded under `oauth.token.issued` (`oauth.token.refreshed` for a
   refresh) with reason `invalid_grant` and a cause: `code_unknown`, `code_reused`,
   `grant_client_mismatch`, `redirect_uri_mismatch`, `pkce_mismatch`, `pkce_required`,
   `pkce_verifier_unexpected`, `refresh_unknown`, `refresh_reused`, `session_ended`,
   `subject_revoked`, `account_unavailable`, `grant_revoked` or `dpop_binding_mismatch`.
2. A reused code or refresh token is recorded instead as `oauth.token.revoked`, raised to
   warning, with a `trigger` of `authorization_code_replay` or `refresh_token_reuse` and
   the grant family it revoked.
3. For `session_ended` or `subject_revoked`, the user's Sessions tab on the dashboard
   shows the logout, administrator action or password reset that ended it.

## Mitigation

- As a client developer: treat any `invalid_grant` on refresh as "sign in again". Store
  the new refresh token from every response, and serialize refreshes per user. Redeem a
  code once, promptly, with the same `redirect_uri` and the verifier of its challenge.
  See [Refresh](https://www.nauthera.io/docs/v1/integration#8-refresh).
- As an operator: a `refresh_token_reuse` revocation from an application that refreshes
  concurrently is a client bug; one from an unfamiliar address may be a stolen token.
- As a user: sign in to the application again.

## Escalation

If codes are refused as `code_unknown` within their lifetime, check that Redis is
reachable from every replica, since codes are kept there. If refusals do not match the
recorded cause, open an issue with the request id, the server version and the cause.
Never attach codes, refresh tokens or verifiers.

---

[OAuth error runbooks](https://www.nauthera.io/docs/v1/runbooks/oauth) · Generated from [`internal/runbook/oauth.yaml`](https://github.com/nauthera/nauthera-server/blob/main/internal/runbook/oauth.yaml) in nauthera-server.
