mastra-ai/mastra · error

State token has expired

Error message

State token has expired

What it means

verifyStateToken parses a signed, base64-encoded state token created during SSO login and returns the original state and redirect URI. This error is thrown when the token's embedded expiry timestamp (payload.e) is in the past, i.e. the login flow took too long between generating the state token (getLoginUrl) and verifying it in the callback. The library expires these state tokens to prevent replay attacks and stale redirect URIs.

Source

Thrown at auth/auth0/src/index.ts:142

  const [payloadB64, signature] = parts as [string, string];

  // Verify signature
  const expectedSig = hmacSign(payloadB64, secret);
  if (!timingSafeEqual(signature, expectedSig)) {
    throw new Error('Invalid or tampered state token');
  }

  // Decode and check expiry
  let payload: StatePayload;
  try {
    payload = JSON.parse(atob(payloadB64)) as StatePayload;
  } catch {
    throw new Error('Invalid state token payload');
  }

  if (payload.e < Date.now()) {
    throw new Error('State token has expired');
  }

  return {
    originalState: payload.s,
    redirectUri: payload.r,
  };
}

/**
 * Simple HMAC-SHA256 using Web Crypto (sync wrapper for predictable use).
 * Returns base64url-encoded signature.
 */
function hmacSign(data: string, secret: string): string {
  // Use a simple hash-based approach that works synchronously
  // This is a simplified HMAC for state tokens (not for long-term secrets)
  const encoder = new TextEncoder();
  const keyBytes = encoder.encode(secret);
  const dataBytes = encoder.encode(data);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Restart the login flow: call getLoginUrl again to generate a fresh signed state token and redirect the user to it
  2. Handle this error in the SSO callback handler by redirecting the user to a fresh login URL instead of failing hard
  3. Check for clock skew (NTP) on the server if expiries appear to happen prematurely
  4. Increase the state token TTL in the auth server config only if the login flow legitimately needs longer than the current window

Example fix

// before
const { originalState, redirectUri } = authServer.redirectUri(state);
// after
let redirect;
try {
  redirect = authServer.redirectUri(state);
} catch (e) {
  if (e.message === 'State token has expired') {
    return Response.redirect(authServer.getLoginUrl('/callback', newState()), 302);
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// State token content is signed+encoded; expiry cannot be checked beforehand without decoding.
// Detect staleness by decoding payload if you control token creation:
function isStateTokenLikelyExpired(signedState: string, ttlMs: number, issuedAt: number) {
  return Date.now() - issuedAt > ttlMs;
}

Try / catch

try {
  const { originalState, redirectUri } = provider.redirectUri(state);
} catch (e) {
  if (e instanceof Error && e.message === 'State token has expired') {
    // restart login flow
    return redirectTo(provider.getLoginUrl(callbackUri, newState()));
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling redirectUri() (which calls verifyStateToken) on a state token whose payload.e is less than Date.now(); typically when the user sits on the Auth0 login page longer than the token TTL before completing login, or when a bookmarked/stale callback URL is replayed after expiry.

Common situations: Users leaving the Auth0 consent screen open past the expiry window; replaying an old callback URL from browser history; long-running approval flows in enterprise SSO; clock skew between server instances if system clocks drift.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/4a8a3b23589424cf. Report an issue: GitHub.