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

  1. Pin AUTH_SECRET in env so it's identical on every instance and across deploys.
  2. Retry sign-in from the app — state is single-use and flow-specific.
  3. Check error.cause to distinguish tampering from secret mismatch.
  4. 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

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


AI-assisted analysis of nextauthjs/next-auth@a1a16a5a77 (2026-08-28). Data as JSON: /api/errors/d91262ab74d868d1. Report an issue: GitHub.