api-server/src/modules/auth/apple-auth.service.ts
WangDL fa69749884 refactor(auth): restructure auth system, align with iOS login flow spec
- Split AuthService into AppleAuthService, TokenService, AuthService
- Add dev-login endpoint (dev-only, disabled in production)
- AppleLoginDto: authorizationCode optional, add userIdentifier/email/fullName/nonce
- Login/refresh responses now include user object
- logout: single-token revoke + JwtAuthGuard protection
- users.repository: switch from in-memory Map to Prisma persistence
- JWT payload includes role, guards attach full user info to request
- Dual JWT secret support (JWT_ACCESS_SECRET / JWT_REFRESH_SECRET)
- Replace jwks-rsa+jsonwebtoken with jose library
- Prisma User model: add role field
- Independent DTO files with @Transform for empty string safety
- Add 5 iOS login flow documentation files
2026-05-13 17:31:50 +08:00

80 lines
2.3 KiB
TypeScript

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<typeof createRemoteJWKSet>;
constructor(private readonly configService: ConfigService) {
this.appleIssuer = this.configService.get<string>(
'apple.issuer',
'https://appleid.apple.com',
);
this.appleBundleId = this.configService.get<string>('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 验证失败');
}
}
}