Skip to content

Commit 5cac897

Browse files
authored
fix: apple sign (#14053)
1 parent 1196101 commit 5cac897

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

packages/backend/server/src/plugins/oauth/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { defineModuleConfig, JSONSchema } from '../../base';
44

55
export interface OAuthProviderConfig {
66
clientId: string;
7-
clientSecret: string;
7+
clientSecret?: string;
88
args?: Record<string, string>;
99
}
1010

packages/backend/server/src/plugins/oauth/providers/apple.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { JsonWebKey } from 'node:crypto';
22

33
import { Injectable } from '@nestjs/common';
44
import jwt, { type JwtPayload } from 'jsonwebtoken';
5+
import { z } from 'zod';
56

67
import {
78
InternalServerError,
@@ -19,14 +20,75 @@ interface AuthTokenResponse {
1920
expires_in: number;
2021
}
2122

23+
const AppleProviderArgsSchema = z.object({
24+
privateKey: z.string().nonempty(),
25+
keyId: z.string().nonempty(),
26+
teamId: z.string().nonempty(),
27+
});
28+
2229
@Injectable()
2330
export class AppleOAuthProvider extends OAuthProvider {
2431
provider = OAuthProviderName.Apple;
32+
private args: z.infer<typeof AppleProviderArgsSchema> | null = null;
33+
private _jwtCache: { token: string; expiresAt: number } | null = null;
2534

2635
constructor(private readonly url: URLHelper) {
2736
super();
2837
}
2938

39+
override get configured() {
40+
if (this.config && !this.args) {
41+
const result = AppleProviderArgsSchema.safeParse(this.config?.args);
42+
if (result.success) {
43+
this.args = result.data;
44+
}
45+
}
46+
47+
return (
48+
!!this.config &&
49+
!!this.config.clientId &&
50+
(!!this.config.clientSecret || !!this.args)
51+
);
52+
}
53+
54+
private get clientSecret() {
55+
if (this.config.clientSecret) {
56+
return this.config.clientSecret;
57+
}
58+
59+
if (!this.args) {
60+
throw new Error('Missing Apple OAuth configuration');
61+
}
62+
63+
if (this._jwtCache && this._jwtCache.expiresAt > Date.now()) {
64+
return this._jwtCache.token;
65+
}
66+
67+
const { privateKey, keyId, teamId } = this.args;
68+
const expiresIn = 300; // 5 minutes
69+
70+
try {
71+
const token = jwt.sign({}, privateKey, {
72+
algorithm: 'ES256',
73+
keyid: keyId,
74+
expiresIn,
75+
issuer: teamId,
76+
audience: 'https://appleid.apple.com',
77+
subject: this.config.clientId,
78+
});
79+
80+
this._jwtCache = {
81+
token,
82+
expiresAt: Date.now() + (expiresIn - 30) * 1000,
83+
};
84+
85+
return token;
86+
} catch (e) {
87+
this.logger.error('Failed to generate Apple client secret JWT', e);
88+
throw new Error('Failed to generate client secret');
89+
}
90+
}
91+
3092
getAuthUrl(state: string, clientNonce?: string): string {
3193
return `https://appleid.apple.com/auth/authorize?${this.url.stringify({
3294
client_id: this.config.clientId,
@@ -46,7 +108,7 @@ export class AppleOAuthProvider extends OAuthProvider {
46108
body: this.url.stringify({
47109
code,
48110
client_id: this.config.clientId,
49-
client_secret: this.config.clientSecret,
111+
client_secret: this.clientSecret,
50112
redirect_uri: this.url.link('/api/oauth/callback'),
51113
grant_type: 'authorization_code',
52114
}),

0 commit comments

Comments
 (0)