mastra-ai/mastra · error

State token has expired

Error message

State token has expired

What it means

verifyStateToken checks the 'e' (expiry) timestamp inside the state payload against Date.now(). The state token embeds a short-lived expiry when created by createStateToken; if the user takes longer than that to complete the OAuth redirect, the token is rejected. This prevents replaying stale state tokens.

Source

Thrown at auth/google/src/auth-provider.ts:206

  if (parts.length !== 2) {
    throw new Error('Invalid state token format');
  }

  const [payloadB64, signature] = parts as [string, string];
  const expectedSig = await hmacSign(payloadB64, secret);
  if (!timingSafeEqual(signature, expectedSig)) {
    throw new Error('Invalid state token signature');
  }

  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,
    nonce: payload.n,
  };
}

function hasExpired(payload: JWTPayload): boolean {
  return typeof payload.exp === 'number' && payload.exp * 1000 < Date.now();
}

export class MastraAuthGoogle extends MastraAuthProvider<GoogleUser> implements IUserProvider<GoogleUser> {
  protected clientId: string;
  private clientSecret: string | null;
  private redirectUri: string | null;
  private scopes: string[];

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Start the login flow again: call getLoginUrl to mint a fresh state token and redirect the user through OAuth again.
  2. If expiry is consistently immediate, check for clock skew between the server that created the token and the one verifying it (sync NTP).
  3. Avoid caching or persisting login URLs/state across sessions; generate a new one per login attempt.
  4. If users routinely exceed the TTL, reduce friction in the consent step or upgrade to a version with an appropriate state TTL.

Example fix

// before (reusing a stored login URL)
const loginUrl = await cache.get('loginUrl');
res.redirect(loginUrl);

// after (fresh URL per request)
const loginUrl = await provider.getLoginUrl(redirectUri, state);
res.redirect(loginUrl);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await provider.verifyStateToken(state);
} catch (err) {
  if (err instanceof Error && err.message === 'State token has expired') {
    // mint a fresh login URL and restart the flow
    const loginUrl = await provider.getLoginUrl(redirectUri, originalState);
    return res.redirect(loginUrl);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling verifyStateToken with a state token whose payload.e timestamp is earlier than the current time — typically because the login URL was generated long before the callback arrived, or a bookmarked/old redirect URL was replayed.

Common situations: User leaves the Google consent page open (or parked) past the token TTL and then completes login; automated tests replaying a recorded state; clock skew between servers; retrying an old callback URL after a failed attempt.

Related errors


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