Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

invalid_grant

Docs version v1 (main)Nauthera is pre-1.0, in a design-partner program

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.
  • 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 · Generated from internal/runbook/oauth.yaml in nauthera-server.