{"record":{"id":"87e2797d9e55efc2","repo":"yikart/AiToEarn","slug":"error-87e279","errorCode":null,"errorMessage":"无效的状态码","messagePattern":"无效的状态码","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts","lineNumber":225,"sourceCode":"      // return response.data;  // 包含新的 access_token、expires_in、token_type 等信息\n    } catch (err) {\n      console.log('Error while refreshing access token', err);\n      throw new Error('Failed to refresh access token');\n    }\n  }\n\n  /**\n   * 验证并保存授权码\n   * @param code 授权码\n   * @param state 状态码\n   * @returns 系统令牌\n   */\n  async handleAuthorizationCode(code: string, state: string, userId: string) {\n    try {\n      // 获取state关联的邮箱信息\n      const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);\n      if (!stateInfo || !stateInfo.mail) {\n        throw new Error('无效的状态码');\n      }\n\n      // 使用授权码获取访问令牌和刷新令牌\n      const params = new URLSearchParams({\n        code: code,\n        redirect_uri: `${this.webRenderBaseUrl}/api/plat/youtube/auth/callback`,\n        client_id: this.webClientId,\n        grant_type: \"authorization_code\",\n        client_secret: this.webClientSecret,\n      });\n\n      const response = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {\n        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n      });\n\n      const { access_token, refresh_token, expires_in, id_token } = response.data;\n\n      // 验证ID令牌以获取用户信息","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts#L207-L243","documentation":"handleAuthorizationCode first validates the OAuth state by reading the Redis key youtube:state:<userId>:<state> populated in getAuthorizationUrl. If the key is missing or has no mail field, it throws Error('无效的状态码') before exchanging the authorization code.","triggerScenarios":"The state was consumed/expired (Redis key TTL is 60*10 seconds = 10 minutes), the callback passes a userId different from the one that started authorization, state was altered/tampered in the redirect, Redis was flushed or restarted without persistence, or getAuthorizationUrl failed to store the key.","commonSituations":"User waits on the Google consent screen longer than 10 minutes then approves; user re-initiates OAuth in a second tab, overwriting/expiring the first state; callback routes to a different user session than the one that generated the URL; Redis restarts losing non-persisted keys.","solutions":["Increase the state TTL in getAuthorizationUrl (currently 60*10 seconds) or regenerate state on retry.","Ensure the callback passes the SAME userId used when generating the authorization URL.","Verify Redis persistence (appendonly/AOF) or accept restarts will invalidate pending OAuth flows.","Have users restart the authorization flow when this error occurs; the code exchange cannot proceed without the state.","Return a clear 'state expired, please re-authorize' response to the frontend instead of a generic error."],"exampleFix":"// before\nthis.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 10);\n// after\nthis.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 30); // 30 min TTL","handlingStrategy":"validation","validationCode":"const stateInfo = await redisService.get(`youtube:state:${userId}:${state}`)\nif (!stateInfo?.mail) {\n  // fail fast with an actionable message before hitting Google\n  return { code: 410, msg: '授权状态已过期，请重新发起授权' }\n}","typeGuard":"const hasValidState = (s: unknown): s is { mail: string } =>\n  typeof s === 'object' && s !== null && typeof (s as any).mail === 'string' && (s as any).mail.length > 0","tryCatchPattern":"try {\n  return await youtubeAuthService.handleAuthorizationCode(code, state, userId)\n} catch (e) {\n  if (e.message === '无效的状态码') {\n    // state expired or userId mismatch; redirect user to restart OAuth, never retry the code\n    return redirect('/youtube/reauthorize')\n  }\n  throw e\n}","preventionTips":["Raise the state TTL above the expected consent-screen dwell time","Pass the originating userId end-to-end through the OAuth round trip","Enable Redis AOF/RDB persistence so restarts don't kill pending flows","Detect and ignore duplicate callback deliveries (second tab, page refresh)","Encode state in the redirect rather than relying on server session identity"],"tags":["oauth","csrf-state","redis","session"],"backgroundTag":"oauth-state-expired","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}