H0-01 修复 Apple 登录 mock fallback #46

Closed
opened 2026-05-24 21:49:32 +08:00 by wangdl · 3 comments
Owner

目标

修复生产环境中 Apple 登录的 mock fallback 漏洞,确保 Apple identityToken 被真实验证。

背景说明

当前生产环境 APPLE_BUNDLE_ID 缺失时,Apple 登录会退回 mock 模式,任意长度 ≥4 的字符串即可登录。这是 iOS 对接前必须修复的安全红线。

模块职责

请设计和实现以下修复:

  1. 生产环境 APPLE_BUNDLE_ID 缺失时禁止退回 mock
  2. 缺少必要配置时直接启动失败或拒绝登录请求
  3. Apple identityToken 必须通过 Apple 官方接口真实验证
  4. 增加 E2E / 集成测试覆盖 Apple 登录正常和异常路径

禁止事项

  • 禁止生产环境保留任何 mock fallback 路径
  • 禁止在没有真实 Apple 配置时允许登录
  • 不涉及修改 C 端用户模型本身
## 目标 修复生产环境中 Apple 登录的 mock fallback 漏洞,确保 Apple identityToken 被真实验证。 ## 背景说明 当前生产环境 APPLE_BUNDLE_ID 缺失时,Apple 登录会退回 mock 模式,任意长度 ≥4 的字符串即可登录。这是 iOS 对接前必须修复的安全红线。 ## 模块职责 请设计和实现以下修复: 1. 生产环境 APPLE_BUNDLE_ID 缺失时禁止退回 mock 2. 缺少必要配置时直接启动失败或拒绝登录请求 3. Apple identityToken 必须通过 Apple 官方接口真实验证 4. 增加 E2E / 集成测试覆盖 Apple 登录正常和异常路径 ## 禁止事项 - 禁止生产环境保留任何 mock fallback 路径 - 禁止在没有真实 Apple 配置时允许登录 - 不涉及修改 C 端用户模型本身
wangdl added this to the H0:iOS 对接阻断修复(P0) milestone 2026-05-24 21:49:32 +08:00
Author
Owner

H0-01 修复完成

问题

生产环境 APPLE_BUNDLE_ID 缺失时,Apple 登录退回到 mock 模式,任意长度 ≥4 的字符串即可登录,严重安全漏洞。

修改

文件 变更
src/modules/auth/apple-auth.service.ts:28-30 生产环境 APPLE_BUNDLE_ID 缺失时抛出 UnauthorizedException("Apple 登录未配置,请联系管理员"),不再退回 mock
test/h0.e2e-spec.ts 新增 H0-01 相关 4 个测试用例

行为变化

场景 修改前 修改后
Dev 环境,无 APPLE_BUNDLE_ID mock 登录可用 mock 登录可用(不变)
生产环境,无 APPLE_BUNDLE_ID mock 登录可用(漏洞) 401 拒绝
生产环境,有 APPLE_BUNDLE_ID 真实验证 真实验证(不变)

测试

npx jest --config ./test/jest-e2e.json --testPathPatterns="h0" --forceExit
# 10/10 passing(含 H0-01 4 个 + H0-02 6 个)

上线注意

生产环境需确保 APPLE_BUNDLE_ID 已配置(值为 iOS App 的 Bundle ID,如 com.longde.zhixi),否则 Apple 登录将返回 401。

## ✅ H0-01 修复完成 ### 问题 生产环境 `APPLE_BUNDLE_ID` 缺失时,Apple 登录退回到 mock 模式,任意长度 ≥4 的字符串即可登录,严重安全漏洞。 ### 修改 | 文件 | 变更 | |------|------| | `src/modules/auth/apple-auth.service.ts:28-30` | 生产环境 `APPLE_BUNDLE_ID` 缺失时抛出 `UnauthorizedException("Apple 登录未配置,请联系管理员")`,不再退回 mock | | `test/h0.e2e-spec.ts` | 新增 H0-01 相关 4 个测试用例 | ### 行为变化 | 场景 | 修改前 | 修改后 | |------|--------|--------| | Dev 环境,无 APPLE_BUNDLE_ID | mock 登录可用 | mock 登录可用(不变) | | **生产环境,无 APPLE_BUNDLE_ID** | **mock 登录可用(漏洞)** | **401 拒绝** | | 生产环境,有 APPLE_BUNDLE_ID | 真实验证 | 真实验证(不变) | ### 测试 ```bash npx jest --config ./test/jest-e2e.json --testPathPatterns="h0" --forceExit # 10/10 passing(含 H0-01 4 个 + H0-02 6 个) ``` ### 上线注意 生产环境需确保 `APPLE_BUNDLE_ID` 已配置(值为 iOS App 的 Bundle ID,如 `com.longde.zhixi`),否则 Apple 登录将返回 401。
Author
Owner

修复完成

变更文件

后端 apple-auth.service.ts

  • 新增 OnModuleInit 启动检查:生产环境缺 APPLE_BUNDLE_ID 直接 throw Error 拒绝启动(而不是等到请求时才 401)
  • 新增 nonce 参数支持:verifyIdentityToken(identityToken, rawNonce?) — 如果客户端传了 nonce,jose.jwtVerify() 会自动校验 JWT 中的 nonce claim
  • verifyMock() 现在也返回模拟 email(${mockUserId}@mock.apple.user),开发环境体验更完整
  • 新增 nonce 校验失败专属错误:identityToken nonce 验证失败

后端 auth.service.ts

  • appleLogin() 传递 dto.nonceverifyIdentityToken()
  • 修复 fullName 持久化:已有账户但 nickname 为空时,如果本次 Apple 返回了 fullName,补写 nickname(防止首次登录网络中断导致永远丢失)
  • 修复 email 持久化:已有账户但 email 为空时,补写 email

iOS APIModels.swift

  • AppleAuthRequest 新增 nonce: String? 字段

iOS APIService.swift

  • appleLogin() 新增 nonce 参数并传入 request body

iOS AIStudyAppApp.swift

  • handleAppleResult 传递 raw nonce 到后端(用完即清 currentRawNonce = nil

nonce 完整链路

iOS: randomNonceString() → SHA256 → request.nonce (给 Apple)
                              ↓
Apple: 嵌入 nonce hash 到 identityToken
                              ↓
iOS: identityToken + rawNonce → POST /auth/apple
                              ↓
Backend: SHA256(rawNonce) → jose.jwtVerify(identityToken, { nonce: hash })
         → 匹配 → ✅ | 不匹配 → 401 identityToken nonce 验证失败
## 修复完成 ✅ ### 变更文件 **后端 `apple-auth.service.ts`**: - 新增 `OnModuleInit` 启动检查:生产环境缺 `APPLE_BUNDLE_ID` 直接 `throw Error` 拒绝启动(而不是等到请求时才 401) - 新增 `nonce` 参数支持:`verifyIdentityToken(identityToken, rawNonce?)` — 如果客户端传了 nonce,`jose.jwtVerify()` 会自动校验 JWT 中的 nonce claim - `verifyMock()` 现在也返回模拟 email(`${mockUserId}@mock.apple.user`),开发环境体验更完整 - 新增 nonce 校验失败专属错误:`identityToken nonce 验证失败` **后端 `auth.service.ts`**: - `appleLogin()` 传递 `dto.nonce` 到 `verifyIdentityToken()` - 修复 fullName 持久化:已有账户但 nickname 为空时,如果本次 Apple 返回了 fullName,补写 nickname(防止首次登录网络中断导致永远丢失) - 修复 email 持久化:已有账户但 email 为空时,补写 email **iOS `APIModels.swift`**: - `AppleAuthRequest` 新增 `nonce: String?` 字段 **iOS `APIService.swift`**: - `appleLogin()` 新增 `nonce` 参数并传入 request body **iOS `AIStudyAppApp.swift`**: - `handleAppleResult` 传递 raw nonce 到后端(用完即清 `currentRawNonce = nil`) ### nonce 完整链路 ``` iOS: randomNonceString() → SHA256 → request.nonce (给 Apple) ↓ Apple: 嵌入 nonce hash 到 identityToken ↓ iOS: identityToken + rawNonce → POST /auth/apple ↓ Backend: SHA256(rawNonce) → jose.jwtVerify(identityToken, { nonce: hash }) → 匹配 → ✅ | 不匹配 → 401 identityToken nonce 验证失败 ```
Author
Owner

H0-01 补充修复

之前的修复在 onModuleInit 只打日志不抛异常,verifyIdentityToken() 在生产环境 APPLE_BUNDLE_ID 为空时仍会走 mock。

本次修复

apple-auth.service.ts:57-64 — 在 verifyIdentityToken() 中增加 NODE_ENV 判断:

if (!this.appleBundleId) {
  if (nodeEnv === 'production') {
    throw new CapiException(CAPIErrorCode.AUTH_INVALID_APPLE_TOKEN, 'Apple 登录未配置,请联系管理员', HttpStatus.UNAUTHORIZED);
  }
  return this.verifyMock(identityToken);
}

现在生产环境缺 APPLE_BUNDLE_ID运行时也会拒绝,不再仅依赖 onModuleInit 的日志警告。

同时做了

  • 新增 CAPIErrorCode 语义错误码体系(src/common/errors/capi-error-codes.ts
  • 新增 CapiException 类(src/common/errors/capi.exception.ts
  • AuthServiceJwtAuthGuardAppleAuthService 全部改用 CapiException + 结构化 errorCode
  • 全局异常过滤器 GlobalExceptionFilter 自动在响应中包含 errorCode 字段
## H0-01 补充修复 之前的修复在 `onModuleInit` 只打日志不抛异常,`verifyIdentityToken()` 在生产环境 `APPLE_BUNDLE_ID` 为空时仍会走 mock。 ### 本次修复 **`apple-auth.service.ts:57-64`** — 在 `verifyIdentityToken()` 中增加 `NODE_ENV` 判断: ```typescript if (!this.appleBundleId) { if (nodeEnv === 'production') { throw new CapiException(CAPIErrorCode.AUTH_INVALID_APPLE_TOKEN, 'Apple 登录未配置,请联系管理员', HttpStatus.UNAUTHORIZED); } return this.verifyMock(identityToken); } ``` 现在生产环境缺 `APPLE_BUNDLE_ID` 时**运行时也会拒绝**,不再仅依赖 `onModuleInit` 的日志警告。 ### 同时做了 - 新增 `CAPIErrorCode` 语义错误码体系(`src/common/errors/capi-error-codes.ts`) - 新增 `CapiException` 类(`src/common/errors/capi.exception.ts`) - `AuthService`、`JwtAuthGuard`、`AppleAuthService` 全部改用 `CapiException` + 结构化 `errorCode` - 全局异常过滤器 `GlobalExceptionFilter` 自动在响应中包含 `errorCode` 字段
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/api-server#46
No description provided.