mastra-ai/mastra · error
State token has expired
Error message
State token has expired
What it means
The Clerk SSO state token is an HMAC-signed, base64-encoded JSON payload that embeds an expiry timestamp `e`. During callback verification, verifyStateToken re-parses the payload and compares the expiry against Date.now(); if the token's epoch-milliseconds have passed, the state can no longer be trusted as a fresh CSRF guard, so the library throws. This is an intentional liveness bound on the OAuth round-trip, not a signature or corruption problem.
Source
Thrown at auth/clerk/src/index.ts:167
*/
async function verifyStateToken(
stateToken: string,
secret: string,
): Promise<{ originalState: string; redirectUri: string }> {
const parts = stateToken.split('.');
if (parts.length !== 2) {
throw new Error('Invalid state token format');
}
const [payloadB64, signature] = parts;
const expectedSig = await hmacSign(payloadB64!, secret);
if (!timingSafeEqual(signature!, expectedSig)) {
throw new Error('Invalid state token signature');
}
const payload = JSON.parse(atob(payloadB64!)) as StatePayload;
if (payload.e < Date.now()) {
throw new Error('State token has expired');
}
return { originalState: payload.s, redirectUri: payload.r };
}
/**
* Escape special regex characters in a string.
*/
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Derive the Frontend API (FAPI) URL from a Clerk publishable key.
* The publishable key is: prefix + base64(fapiDomain + "$")
*/
function deriveFapiUrl(publishableKey: string): string {
const withoutPrefix = publishableKey.replace(/^pk_(test|live)_/, '');View on GitHub (pinned to 75dd419e61)
Solutions
- Have the user restart the SSO login flow so a fresh state token is issued.
- Increase the state-token TTL via the provider's configuration/options if legitimate callbacks are slow.
- Check server clocks (NTP) on all instances to eliminate skew between sign and verify.
- Improve UX by handling the thrown error and redirecting the user back to the login initiation instead of showing a raw error.
Example fix
// before: raw error surfaces on callback
await provider.verifyCallbackState(state);
// after: restart flow on expiry
try {
await provider.verifyCallbackState(state);
} catch (e) {
if ((e as Error).message === 'State token has expired') {
return res.redirect('/auth/sso/login'); // re-initiate
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
function isLikelyExpired(stateToken: string): boolean {
try {
const payload = JSON.parse(atob(stateToken.split('.')[0]));
return typeof payload.e === 'number' && payload.e < Date.now();
} catch {
return false;
}
} Try / catch
try {
await provider.verifyCallbackState(state);
} catch (e) {
if ((e as Error).message === 'State token has expired') {
return restartLoginFlow(); // redirect to SSO initiation
}
throw e;
} Prevention
- Set a state-token TTL comfortably longer than the slowest realistic OAuth round-trip.
- Sync server clocks with NTP across all instances.
- Auto-redirect to login re-initiation on expiry instead of surfacing a raw error.
When it happens
Trigger: A user starts SSO login via _attachSSOProvider's getAuthorizationUri, which calls createStateToken with a TTL, but the provider's callback arrives after that TTL and redirectUri → verifyStateToken runs `payload.e < Date.now()` → throw.
Common situations: User leaves the login page open and resumes hours later; very long serverless cold starts or queues delaying the callback; clock skew between the node that signed the token and the node verifying it; an unusually short expiry configured for the state token.
Related errors
- Redirect URI is required for SSO login
- Redirect URI is required for SSO. Set AUTH0_REDIRECT_URI or
- Cookie password must be at least 32 characters for SSO. Set
- Token exchange failed: ${error}
- Failed to fetch user info from Clerk
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/503616e4c0330c7e.
Report an issue: GitHub.