{"record":{"id":"1641ded8e7283a30","repo":"mastra-ai/mastra","slug":"state-token-has-expired-1641de","errorCode":null,"errorMessage":"State token has expired","messagePattern":"State token has expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"auth/google/src/auth-provider.ts","lineNumber":206,"sourceCode":"  if (parts.length !== 2) {\n    throw new Error('Invalid state token format');\n  }\n\n  const [payloadB64, signature] = parts as [string, string];\n  const expectedSig = await hmacSign(payloadB64, secret);\n  if (!timingSafeEqual(signature, expectedSig)) {\n    throw new Error('Invalid state token signature');\n  }\n\n  let payload: StatePayload;\n  try {\n    payload = JSON.parse(atob(payloadB64)) as StatePayload;\n  } catch {\n    throw new Error('Invalid state token payload');\n  }\n\n  if (payload.e < Date.now()) {\n    throw new Error('State token has expired');\n  }\n\n  return {\n    originalState: payload.s,\n    redirectUri: payload.r,\n    nonce: payload.n,\n  };\n}\n\nfunction hasExpired(payload: JWTPayload): boolean {\n  return typeof payload.exp === 'number' && payload.exp * 1000 < Date.now();\n}\n\nexport class MastraAuthGoogle extends MastraAuthProvider<GoogleUser> implements IUserProvider<GoogleUser> {\n  protected clientId: string;\n  private clientSecret: string | null;\n  private redirectUri: string | null;\n  private scopes: string[];","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/auth/google/src/auth-provider.ts#L188-L224","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (reusing a stored login URL)\nconst loginUrl = await cache.get('loginUrl');\nres.redirect(loginUrl);\n\n// after (fresh URL per request)\nconst loginUrl = await provider.getLoginUrl(redirectUri, state);\nres.redirect(loginUrl);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await provider.verifyStateToken(state);\n} catch (err) {\n  if (err instanceof Error && err.message === 'State token has expired') {\n    // mint a fresh login URL and restart the flow\n    const loginUrl = await provider.getLoginUrl(redirectUri, originalState);\n    return res.redirect(loginUrl);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["oauth","csrf","state-token","expired","timeout"],"backgroundTag":"jwt-token-expired","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}