You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Node.js中验证Apple的idToken与authorizationCode的安全方案

问题描述

客户端完成Apple登录流程后,向服务端发送了Apple的identityToken和authorizationCode。目前我仅通过以下代码验证idToken的签名:

async appleLogin(appleLoginDto: AppleLoginDto) {
    const id_token = appleLoginDto.id_token;
    const { header } = jwt.decode(id_token, {
      complete: true,
    });
    const kid = header.kid;
    const publicKey = (await this.key(kid)).getPublicKey();
    const test = jwt.verify(id_token, publicKey);
    return test
  }

  async key(kid) {
    const client = jwksClient({
      jwksUri: 'https://appleid.apple.com/auth/keys',
      timeout: 30000,
    });

    return await client.getSigningKey(kid);
  }

但我认为当前的验证逻辑不够充分,请问如何结合Authorization Code与JWT内容,实现更安全的Apple登录功能?

实现安全Apple登录的完整方案

要实现安全的Apple登录,必须同时验证authorizationCode和idToken,并对JWT的核心声明做校验,具体步骤如下:

1. 验证Authorization Code的有效性

向Apple的Token验证端点发起POST请求,用authorizationCode换取官方颁发的id_token、access_token和refresh_token。这一步能确保客户端传来的code是真实有效的,避免伪造的code绕过验证。

示例代码:

async verifyAuthorizationCode(authorizationCode: string, clientId: string, clientSecret: string) {
  const response = await fetch('https://appleid.apple.com/auth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: authorizationCode,
      client_id: clientId, // 你的Apple开发者账号客户端ID
      client_secret: clientSecret, // 用Apple私钥生成的客户端密钥
    }),
  });

  if (!response.ok) {
    throw new Error('Authorization Code验证失败');
  }

  return await response.json();
}

2. 对返回的id_token做全面JWT校验

不要只验证签名,必须校验JWT的核心声明(Claims),确保token符合预期:

  • iss:必须等于https://appleid.apple.com,验证签发方为Apple
  • aud:必须匹配你的客户端ID,确保token是发给你的应用的
  • exp:检查token是否过期
  • iat:验证签发时间是否合理(不能是未来时间)
  • nonce:如果客户端发起登录时生成了nonce,必须和id_token里的nonce一致,防止重放攻击
  • sub:确保该用户标识和客户端传来的id_token中的sub一致,确认是同一用户

修改原验证逻辑,加入完整校验:

async appleLogin(appleLoginDto: AppleLoginDto) {
  const { id_token: clientIdToken, authorization_code: authCode } = appleLoginDto;
  const clientId = '你的Apple客户端ID';
  const clientSecret = '你的Apple客户端密钥';

  // 1. 验证Authorization Code,获取官方返回的id_token
  const tokenResponse = await this.verifyAuthorizationCode(authCode, clientId, clientSecret);
  const serverIdToken = tokenResponse.id_token;

  // 2. 获取Apple公钥,验证serverIdToken的签名
  const { header } = jwt.decode(serverIdToken, { complete: true }) as { header: { kid: string } };
  const publicKey = (await this.key(header.kid)).getPublicKey();
  
  // 3. 全面校验JWT声明
  const verifiedClaims = jwt.verify(serverIdToken, publicKey, {
    issuer: 'https://appleid.apple.com',
    audience: clientId,
    // 若使用nonce,取消注释下面一行
    // nonce: appleLoginDto.nonce,
  }) as { sub: string; email?: string; exp: number; iat: number };

  // 4. 校验客户端传来的id_token的sub是否与官方返回一致
  const clientClaims = jwt.decode(clientIdToken) as { sub: string };
  if (clientClaims.sub !== verifiedClaims.sub) {
    throw new Error('用户标识不匹配,登录无效');
  }

  // 5. 关联或创建本地用户,生成应用自有会话token
  const user = await this.findOrCreateUser(verifiedClaims.sub, verifiedClaims.email);
  const sessionToken = this.generateSessionToken(user);

  return { sessionToken, user };
}

async key(kid: string) {
  const client = jwksClient({
    jwksUri: 'https://appleid.apple.com/auth/keys',
    timeout: 30000,
  });

  return await client.getSigningKey(kid);
}

3. 额外安全注意事项

  • 客户端密钥管理:必须用Apple开发者账号提供的私钥生成client_secret,定期轮换,禁止硬编码在代码中
  • nonce强制使用:客户端发起登录时生成随机nonce并存入本地存储,服务端必须校验该值,杜绝重放攻击
  • 用户关联逻辑:用sub(Apple用户唯一标识)作为本地用户关联主键,不要依赖邮箱(用户可能隐藏或更改邮箱)
  • 会话token生成:不要将Apple的id_token或access_token返回给客户端长期使用,生成应用自有会话token,便于权限控制和过期管理

内容的提问来源于stack exchange,提问作者Mevo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 09:26:35