@@ -235,7 +235,7 @@ export async function verifyAnyGoogleToken(
235235 const { payload : peek } = parseJwtUnverified ( idToken ) ;
236236
237237 if ( peek . iss === `${ FIREBASE_TOKEN_ISSUER_PREFIX } ${ firebaseProjectId } ` ) {
238- // Standard Firebase ID token
238+ // Standard Firebase ID token (from web Firebase popup)
239239 return verifyFirebaseIdToken ( idToken , firebaseProjectId ) ;
240240 }
241241
@@ -244,9 +244,97 @@ export async function verifyAnyGoogleToken(
244244 return verifyGoogleIdToken ( idToken , allowedGoogleClientIds ) ;
245245 }
246246
247+ if ( peek . iss === "https://appleid.apple.com" ) {
248+ // Native Apple ID token (from iOS Sign in with Apple)
249+ return verifyAppleIdToken ( idToken , [ ] ) ;
250+ }
251+
247252 throw new Error ( `Unrecognized token issuer: ${ peek . iss } ` ) ;
248253}
249254
255+ // ---------------------------------------------------------------------------
256+ // Apple ID Token verification (for native iOS Sign in with Apple)
257+ // ---------------------------------------------------------------------------
258+
259+ const APPLE_JWKS_URL = "https://appleid.apple.com/auth/keys" ;
260+
261+ let cachedAppleKeys : JsonWebKey [ ] | null = null ;
262+ let cachedAppleAt = 0 ;
263+
264+ async function getApplePublicKeys ( ) : Promise < JsonWebKey [ ] > {
265+ const now = Date . now ( ) ;
266+ if ( cachedAppleKeys && now - cachedAppleAt < CACHE_TTL_MS ) {
267+ return cachedAppleKeys ;
268+ }
269+ const resp = await fetch ( APPLE_JWKS_URL ) ;
270+ if ( ! resp . ok ) {
271+ throw new Error ( `Failed to fetch Apple JWKS: ${ resp . status } ` ) ;
272+ }
273+ const jwks = ( await resp . json ( ) ) as { keys : JsonWebKey [ ] } ;
274+ cachedAppleKeys = jwks . keys ;
275+ cachedAppleAt = now ;
276+ return jwks . keys ;
277+ }
278+
279+ export async function verifyAppleIdToken (
280+ idToken : string ,
281+ allowedAudiences : string [ ] ,
282+ ) : Promise < FirebaseTokenPayload > {
283+ const { header, payload, signatureBytes, signedContent } =
284+ parseJwtUnverified ( idToken ) ;
285+
286+ if ( header . alg !== "RS256" ) {
287+ throw new Error ( `Unsupported algorithm: ${ header . alg } ` ) ;
288+ }
289+
290+ let keys = await getApplePublicKeys ( ) ;
291+ let matchingKey = keys . find ( ( k ) => ( k as { kid ?: string } ) . kid === header . kid ) ;
292+ if ( ! matchingKey ) {
293+ cachedAppleKeys = null ;
294+ keys = await getApplePublicKeys ( ) ;
295+ matchingKey = keys . find ( ( k ) => ( k as { kid ?: string } ) . kid === header . kid ) ;
296+ if ( ! matchingKey ) {
297+ throw new Error ( `No matching Apple key for kid: ${ header . kid } ` ) ;
298+ }
299+ }
300+
301+ const key = await crypto . subtle . importKey (
302+ "jwk" , matchingKey ,
303+ { name : "RSASSA-PKCS1-v1_5" , hash : "SHA-256" } ,
304+ false , [ "verify" ] ,
305+ ) ;
306+
307+ const valid = await crypto . subtle . verify (
308+ "RSASSA-PKCS1-v1_5" , key , signatureBytes ,
309+ new TextEncoder ( ) . encode ( signedContent ) ,
310+ ) ;
311+ if ( ! valid ) throw new Error ( "Invalid Apple token signature" ) ;
312+
313+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
314+ if ( payload . exp < now ) throw new Error ( "Apple token has expired" ) ;
315+ if ( payload . iat > now + 300 ) throw new Error ( "Apple token issued in the future" ) ;
316+
317+ if ( payload . iss !== "https://appleid.apple.com" ) {
318+ throw new Error ( `Invalid Apple token issuer: ${ payload . iss } ` ) ;
319+ }
320+
321+ if ( allowedAudiences . length > 0 && ! allowedAudiences . includes ( payload . aud ) ) {
322+ throw new Error ( `Invalid Apple token audience: ${ payload . aud } ` ) ;
323+ }
324+
325+ if ( ! payload . sub ) throw new Error ( "Missing subject in Apple token" ) ;
326+
327+ // Synthesize firebase-like fields so the rest of the auth flow works
328+ if ( ! payload . firebase ) {
329+ payload . firebase = {
330+ sign_in_provider : "apple.com" ,
331+ identities : { "apple.com" : [ payload . sub ] } ,
332+ } ;
333+ }
334+
335+ return payload ;
336+ }
337+
250338// ---------------------------------------------------------------------------
251339// Shared verification helpers
252340// ---------------------------------------------------------------------------
0 commit comments