import * as crypto from 'crypto'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { createRemoteJWKSet, jwtVerify } from 'jose'; @Injectable() export class AppleAuthService { private readonly appleIssuer: string; private readonly appleBundleId: string; private readonly jwks: ReturnType; constructor(private readonly configService: ConfigService) { this.appleIssuer = this.configService.get( 'apple.issuer', 'https://appleid.apple.com', ); this.appleBundleId = this.configService.get('apple.bundleId', ''); this.jwks = createRemoteJWKSet( new URL('https://appleid.apple.com/auth/keys'), ); } async verifyIdentityToken(identityToken: string): Promise<{ appleUserId: string; email?: string; emailVerified?: boolean; }> { if (!this.appleBundleId) { return this.verifyMock(identityToken); } return this.verifyReal(identityToken); } private verifyMock(identityToken: string): { appleUserId: string; } { if (!identityToken || identityToken.trim().length < 4) { throw new UnauthorizedException('identityToken 无效'); } return { appleUserId: crypto .createHash('sha256') .update(`apple-mock:${identityToken}`) .digest('hex') .slice(0, 64), }; } private async verifyReal(identityToken: string): Promise<{ appleUserId: string; email?: string; emailVerified?: boolean; }> { try { const { payload } = await jwtVerify(identityToken, this.jwks, { issuer: this.appleIssuer, audience: this.appleBundleId, }); return { appleUserId: payload.sub!, email: typeof payload.email === 'string' ? payload.email : undefined, emailVerified: payload.email_verified === true || payload.email_verified === 'true', }; } catch (err: any) { const msg: string = err?.message ?? ''; if (msg.includes('audience')) { throw new UnauthorizedException( `identityToken audience 不匹配,期望 ${this.appleBundleId}`, ); } if (msg.includes('issuer')) { throw new UnauthorizedException('identityToken issuer 无效'); } throw new UnauthorizedException('identityToken 验证失败'); } } }