nextauthjs/next-auth · error · InvalidCheck
State could not be decoded
Error message
State could not be decoded
What it means
state.decode wraps any failure while decoding the state token (including the internal "Invalid state" throw) into InvalidCheck with the message "State could not be decoded" and the original error as `cause`. This is the error developers actually observe when state validation fails at the callback.
Source
Thrown at packages/core/src/lib/actions/callback/oauth/checks.ts:202
/**
* Returns state if the provider is configured to use state,
* and clears the container cookie afterwards.
* An error is thrown if the state is missing or invalid.
*/
use: useCookie("state", "state"),
/** Decodes the state. If it could not be decoded, it throws an error. */
async decode(state: string, options: InternalOptions) {
try {
options.logger.debug("DECODE_STATE", { state })
const payload = await decode<EncodedState>({
secret: options.jwt.secret,
token: state,
salt: encodedStateSalt,
})
if (payload) return payload
throw new Error("Invalid state")
} catch (error) {
throw new InvalidCheck("State could not be decoded", { cause: error })
}
},
}
export const nonce = {
async create(options: InternalOptions<"oidc">) {
if (!options.provider.checks.includes("nonce")) return
const value = o.generateRandomNonce()
const cookie = await sealCookie("nonce", value, options)
return { cookie, value }
},
/**
* Returns nonce if the provider is configured to use nonce,
* and clears the container cookie afterwards.
* An error is thrown if the nonce is missing or invalid.
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
*/View on GitHub (pinned to a1a16a5a77)
Solutions
- Pin AUTH_SECRET in env so it's identical on every instance and across deploys.
- Retry sign-in from the app — state is single-use and flow-specific.
- Check error.cause to distinguish tampering from secret mismatch.
- Ensure proxies don't drop the state query parameter or cookies during the redirect chain.
Example fix
// before: ephemeral secret per boot
const secret = process.env.NODE_ENV === "production" ? undefined : "dev"
// after: explicit persistent secret
export const { handlers } = NextAuth({
secret: process.env.AUTH_SECRET,
}) Defensive patterns
Strategy: try-catch
Validate before calling
if (!process.env.AUTH_SECRET) throw new Error("AUTH_SECRET required")
// verify all instances agree
// e.g. log a hash of the secret at boot and compare across pods Try / catch
try {
const result = await handlers.GET(req)
} catch (e) {
if (e?.message?.includes("State could not be decoded")) {
const cause = e.cause // jwt signature/parse error details
return Response.redirect("/signin?error=StateMismatch")
}
} Prevention
- Pin AUTH_SECRET in environment config, identical on every replica
- Avoid deploys/secret rotations while sign-in flows are in progress
- Ensure reverse proxies preserve the state query parameter and cookies
When it happens
Trigger: Callback receives a `state` query parameter that fails JWT decode/verification against options.jwt.secret with the encodedStateSalt — signature mismatch, malformed token, expired state, or state generated with a different secret.
Common situations: Secret mismatch between sign-in and callback servers (load-balanced fleet, per-boot generated secrets); state cookie/token lost or stripped by proxy; user leaving the provider login page open long enough to cross a secret rotation/deploy; browsers pre-fetching or replaying the callback.
Related errors
- Invalid state
- OAuth Provider returned an error
- ${name} cookie was missing
- State data was provided but the provider is not configured t
- Account not created
AI-assisted analysis of nextauthjs/next-auth@a1a16a5a77 (2026-08-28).
Data as JSON: /api/errors/d91262ab74d868d1.
Report an issue: GitHub.