yikart/AiToEarn · error · Error

无效的状态码

Error message

无效的状态码

What it means

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.

Source

Thrown at project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts:225

      // return response.data;  // 包含新的 access_token、expires_in、token_type 等信息
    } catch (err) {
      console.log('Error while refreshing access token', err);
      throw new Error('Failed to refresh access token');
    }
  }

  /**
   * 验证并保存授权码
   * @param code 授权码
   * @param state 状态码
   * @returns 系统令牌
   */
  async handleAuthorizationCode(code: string, state: string, userId: string) {
    try {
      // 获取state关联的邮箱信息
      const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);
      if (!stateInfo || !stateInfo.mail) {
        throw new Error('无效的状态码');
      }

      // 使用授权码获取访问令牌和刷新令牌
      const params = new URLSearchParams({
        code: code,
        redirect_uri: `${this.webRenderBaseUrl}/api/plat/youtube/auth/callback`,
        client_id: this.webClientId,
        grant_type: "authorization_code",
        client_secret: this.webClientSecret,
      });

      const response = await axios.post('https://oauth2.googleapis.com/token', params.toString(), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      });

      const { access_token, refresh_token, expires_in, id_token } = response.data;

      // 验证ID令牌以获取用户信息

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Increase the state TTL in getAuthorizationUrl (currently 60*10 seconds) or regenerate state on retry.
  2. Ensure the callback passes the SAME userId used when generating the authorization URL.
  3. Verify Redis persistence (appendonly/AOF) or accept restarts will invalidate pending OAuth flows.
  4. Have users restart the authorization flow when this error occurs; the code exchange cannot proceed without the state.
  5. Return a clear 'state expired, please re-authorize' response to the frontend instead of a generic error.

Example fix

// before
this.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 10);
// after
this.redisService.setKey(`youtube:state:${userId}:${state}`, { mail }, 60 * 30); // 30 min TTL
Defensive patterns

Strategy: validation

Validate before calling

const stateInfo = await redisService.get(`youtube:state:${userId}:${state}`)
if (!stateInfo?.mail) {
  // fail fast with an actionable message before hitting Google
  return { code: 410, msg: '授权状态已过期,请重新发起授权' }
}

Type guard

const hasValidState = (s: unknown): s is { mail: string } =>
  typeof s === 'object' && s !== null && typeof (s as any).mail === 'string' && (s as any).mail.length > 0

Try / catch

try {
  return await youtubeAuthService.handleAuthorizationCode(code, state, userId)
} catch (e) {
  if (e.message === '无效的状态码') {
    // state expired or userId mismatch; redirect user to restart OAuth, never retry the code
    return redirect('/youtube/reauthorize')
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/87e2797d9e55efc2. Report an issue: GitHub.