mastra-ai/mastra · error

Invalid or expired state parameter

Error message

Invalid or expired state parameter

What it means

Thrown by handleCallback when the stateId passed by the server does not exist in the in-memory stateStore. State is stored when the SSO flow starts and looked up (then deleted) on callback; a miss means the state was never created, was already consumed, or the process restarted (in-memory store lost).

Source

Thrown at auth/okta/src/auth-provider.ts:414

      client_id: this.clientId,
      response_type: 'code',
      scope: this.scopes.join(' '),
      redirect_uri: actualRedirectUri,
      state,
    });

    return `${this.endpointBase}/v1/authorize?${params.toString()}`;
  }

  /**
   * Handle the OAuth callback from Okta.
   * Note: The server passes only the stateId (UUID part), not the full state.
   */
  async handleCallback(code: string, stateId: string): Promise<SSOCallbackResult<OktaUser>> {
    // Validate state parameter (server passes only the UUID part)
    const stored = stateStore.get(stateId);
    if (!stored) {
      throw new Error('Invalid or expired state parameter');
    }
    stateStore.delete(stateId);

    if (stored.expiresAt < Date.now()) {
      throw new Error('State parameter has expired');
    }

    // Exchange code for tokens using client_secret (confidential client)
    const tokenResponse = await fetch(`${this.endpointBase}/v1/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`,
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: stored.redirectUri,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Have the user restart the SSO login flow from the beginning (fresh authorize redirect generates new state).
  2. Avoid replaying the callback — the state is single-use and deleted on first consumption.
  3. For multi-instance deployments, use a shared store (Redis/DB) for state or sticky sessions.
  4. Check that the authorize flow start and callback hit the same server process in development.
Defensive patterns

Strategy: try-catch

Validate before calling

if (!stateId || typeof stateId !== 'string') {
  return res.status(400).json({ error: 'Missing state parameter' });
}

Try / catch

try {
  await provider.handleCallback(code, stateId);
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid or expired state parameter') {
    // redirect user to restart SSO: /api/auth/sso/okta/authorize
  } else throw e;
}

Prevention

When it happens

Trigger: Calling handleCallback(code, stateId) with a stateId that is not in stateStore: repeated callback (state deleted after first use), server restart between authorize redirect and callback, multiple server instances (state stored on another instance), or a forged/CSRF state value.

Common situations: User refreshes the callback URL and replays the request; horizontal scaling without a shared state store; dev server hot-reload clearing module memory; load balancer routing callback to a different pod.

Related errors


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