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
- Start the login flow again: call getLoginUrl to mint a fresh state token and redirect the user through OAuth again.
- If expiry is consistently immediate, check for clock skew between the server that created the token and the one verifying it (sync NTP).
- Avoid caching or persisting login URLs/state across sessions; generate a new one per login attempt.
- 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
- Generate a fresh login URL for every login attempt; never cache or bookmark them.
- Sync server clocks with NTP to avoid premature expiry from skew.
- Keep users moving through consent promptly; treat expired-state errors as 'restart login', not retry.
- In tests, mint state tokens immediately before use.
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
- Invalid state token format
- Invalid state token payload
- Invalid or tampered state token
- Invalid state token payload
- Invalid state token format
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/1641ded8e7283a30.
Report an issue: GitHub.