mastra-ai/mastra · error · Error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

Thrown by handleCallback when the HTTP POST to Okta's /v1/token endpoint (exchanging the authorization code for tokens using client_secret) returns a non-OK response. The error message embeds Okta's raw error body, typically invalid_grant or invalid_client.

Source

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

    }

    // 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,
      }),
    });

    if (!tokenResponse.ok) {
      const error = await tokenResponse.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    const tokens = (await tokenResponse.json()) as {
      access_token: string;
      id_token: string;
      refresh_token?: string;
      expires_in: number;
      token_type: string;
    };

    // Verify and decode ID token
    const { payload: idTokenPayload } = await jwtVerify(tokens.id_token, this.jwks, {
      issuer: this.issuer,
      audience: this.clientId,
    });
    const user = mapOktaClaimsToUser(idTokenPayload);

    // Create encrypted session cookie.

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Read the embedded error body: invalid_grant means restart the login flow (code is single-use/expired); invalid_client means fix clientId/clientSecret.
  2. Ensure the redirect_uri sent to the token endpoint exactly matches the authorize request and the Okta app whitelist.
  3. Verify OKTA_CLIENT_SECRET and OKTA_CLIENT_ID are current and for the correct Okta domain/app.
  4. Check Okta system logs (admin console) for the failed token request details; retry after transient 5xx/outage.

Example fix

// before (mismatched redirect between authorize and token exchange)
redirectUri: 'https://localhost:3000/callback'
// after (use the exact same, whitelisted URI in both steps and Okta app settings)
redirectUri: process.env.OKTA_REDIRECT_URI // e.g. https://app.example.com/api/auth/sso/okta/callback
Defensive patterns

Strategy: try-catch

Validate before calling

// before starting the flow, ensure redirect URI matches the one the callback will send
if (authorizeRedirectUri !== process.env.OKTA_REDIRECT_URI) {
  throw new Error('redirect_uri mismatch between authorize and token exchange');
}

Try / catch

try {
  await provider.handleCallback(code, stateId);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Token exchange failed:')) {
    if (e.message.includes('invalid_grant')) {
      // restart SSO flow — code is single-use or expired
    } else if (e.message.includes('invalid_client')) {
      // check OKTA_CLIENT_ID / OKTA_CLIENT_SECRET / domain
    }
  } else throw e;
}

Prevention

When it happens

Trigger: Token exchange fails because: the authorization code was already used or expired (invalid_grant), redirect_uri doesn't exactly match the one used in the authorize request, clientId/clientSecret are wrong (invalid_client), or the Okta app/endpoint is misconfigured or unreachable.

Common situations: Callback replayed after refresh (code single-use); environment mismatch where callback uses a different redirect URI than the authorize request; rotated/incorrect client secret; Okta outage returning 5xx; dev/prod Okta domain mixups.

Related errors


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