api-server/src/modules/auth/apple-auth.service.ts

80 lines
2.3 KiB
TypeScript
Raw Normal View History

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 验证失败');
}
}
}