@@ -2,6 +2,7 @@ import { JsonWebKey } from 'node:crypto';
22
33import { Injectable } from '@nestjs/common' ;
44import jwt , { type JwtPayload } from 'jsonwebtoken' ;
5+ import { z } from 'zod' ;
56
67import {
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 ( )
2330export 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