{"record":{"id":"347f6fd40bb5040e","repo":"mastra-ai/mastra","slug":"google-token-exchange-failed-error","errorCode":null,"errorMessage":"Google token exchange failed: ${error}","messagePattern":"Google token exchange failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"auth/google/src/auth-provider.ts","lineNumber":490,"sourceCode":"      const { originalState, redirectUri, nonce } = await verifyStateToken(signedState, self.cookiePassword);\n      verifyCallbackStateSuffix(callbackState, originalState);\n\n      const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n        body: new URLSearchParams({\n          grant_type: 'authorization_code',\n          code,\n          client_id: self.clientId,\n          client_secret: self.clientSecret!,\n          redirect_uri: redirectUri,\n        }),\n        signal: AbortSignal.timeout(10_000),\n      });\n\n      if (!tokenResponse.ok) {\n        const error = await tokenResponse.text();\n        throw new Error(`Google token exchange failed: ${error}`);\n      }\n\n      const tokens = (await tokenResponse.json()) as {\n        access_token: string;\n        id_token?: string;\n        refresh_token?: string;\n        expires_in: number;\n        token_type: string;\n      };\n\n      if (!tokens.id_token) {\n        throw new Error('Google token response did not include an ID token');\n      }\n\n      const user = await self.verifyIdToken(tokens.id_token, nonce);\n      const sessionData = {\n        user,\n        expiresAt: Date.now() + self.cookieMaxAge * 1000,","sourceCodeStart":472,"sourceCodeEnd":508,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/auth/google/src/auth-provider.ts#L472-L508","documentation":"Thrown by MastraAuthGoogle.handleCallback when the POST to Google's OAuth token endpoint (https://oauth2.googleapis.com/token) returns a non-OK HTTP status during the authorization-code exchange. The response body (Google's JSON error, e.g. invalid_grant) is appended to the message. It means Google rejected the code/credentials/redirect_uri combination, so no tokens were issued.","triggerScenarios":"Calling handleCallback(code, callbackState) when Google returns an error status: the authorization code was already redeemed or expired (invalid_grant), client_id/client_secret are wrong (invalid_client), the redirect_uri differs from the one used in the authorization URL, the code was tampered with, or Google is returning 5xx.","commonSituations":"Replaying a callback (browser refresh double-redeems the code); mismatched redirect URI between the authorize step and the token exchange (e.g. localhost vs production domain not registered in Google Cloud Console); wrong GOOGLE_CLIENT_SECRET or swapped env values between environments; clock skew/expired code from slow redirects.","solutions":["Verify the redirect_uri used in getAuthorizationUrl exactly matches the one sent to the token exchange and is registered in Google Cloud Console (OAuth client authorized redirect URIs).","Check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are correct for the same OAuth client and environment.","Don't retry with a consumed code: the code is single-use (invalid_grant); restart the OAuth flow with a fresh authorization URL.","Read the appended Google error body in the message to identify the exact OAuth error code (invalid_client, invalid_grant, redirect_uri_mismatch, etc.).","If 5xx/internal errors, retry the full OAuth flow; Google may have transient issues."],"exampleFix":"// before (common bug: different redirect URI at token exchange)\nconst tokens = await exchange(code, { redirect_uri: 'https://prod.example.com/callback' });\n// after: reuse the exact URI from the authorize step\nconst redirectUri = self.redirectUri; // same value put in the authorize URL\nconst tokens = await exchange(code, { redirect_uri: redirectUri });","handlingStrategy":"try-catch","validationCode":"if (!clientId || !clientSecret) throw new Error('Google OAuth client credentials required before starting SSO');\nconst redirectUri = new URL(callbackUrl, baseUrl).toString();\nif (!registeredRedirectUris.includes(redirectUri)) throw new Error(`redirect_uri ${redirectUri} not registered in Google Cloud Console`);","typeGuard":"function isGoogleTokenError(err: unknown): err is Error & { message: string } {\n  return err instanceof Error && err.message.startsWith('Google token exchange failed:');\n}","tryCatchPattern":"try {\n  const result = await provider.handleCallback(code, state);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Google token exchange failed:')) {\n    // log err.message (contains Google's error body: invalid_grant, redirect_uri_mismatch, ...)\n    // code is single-use — redirect the user to a fresh authorization URL; never reuse `code`\n    return redirectTo(provider.getAuthorizationUrl(state));\n  }\n  throw err;\n}","preventionTips":["Keep a single source of truth for redirect_uri and use it in both the authorize and token steps","Register all environment callback URLs in Google Cloud Console","Never retry with the same authorization code; codes are single-use","Check GOOGLE_CLIENT_SECRET per environment before deploy","Surface the appended Google error body in logs to speed diagnosis"],"tags":["oauth","google","network","sso"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}