UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

119 lines (107 loc) 3.79 kB
import { $context, AlephaError, z } from "alepha"; import type { IssuerPrimitive } from "alepha/security"; import type { OAuth2Profile } from "../providers/ServerAuthProvider.ts"; import { $auth, type LinkAccountFn, type LinkAccountOptions, type OidcOptions, type WithLinkFn, } from "./$auth.ts"; /** * Already configured Apple authentication primitive. * * Uses OpenID Connect (OIDC) to authenticate users via their Apple accounts. * Upon successful authentication, it links the Apple account to a user session. * * Apple-specific behavior: * - `response_mode=form_post` (required by Apple when requesting `email`/`name`). * - Scope: `name email` (Apple does not support the standard `profile` scope). * - The user's name is only provided on the first authorization, as a `user` * form field on the POST callback. The framework extracts it and injects * `given_name` / `family_name` / `name` into the profile before linking. * Subsequent logins only return `sub` and `email` in the ID token. * - `email_verified` and `is_private_email` are normalized from Apple's * string ("true"/"false") representation to booleans. * * Client secret: * Apple requires the client secret to be a signed ES256 JWT generated from * your Apple private key, team ID, and key ID. This JWT is valid for up to 6 * months; you must rotate it before expiration. Generate it out of band and * set it via `APPLE_CLIENT_SECRET`. * * See: https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret * * Environment Variables: * - `APPLE_CLIENT_ID`: The Service ID obtained from the Apple Developer Console. * - `APPLE_CLIENT_SECRET`: The signed ES256 JWT client secret generated from your * Apple private key. */ export const $authApple = ( realm: IssuerPrimitive & WithLinkFn, options: Partial<OidcOptions> = {}, ) => { const { alepha } = $context(); const env = alepha.parseEnv( z.object({ APPLE_CLIENT_ID: z .text({ description: "The Service ID obtained from the Apple Developer Console.", }) .optional(), APPLE_CLIENT_SECRET: z .text({ description: "The signed ES256 JWT client secret generated from your Apple private key.", }) .optional(), }), ); const disabled = !env.APPLE_CLIENT_ID || !env.APPLE_CLIENT_SECRET; const name = "apple"; const userAccount: LinkAccountFn | undefined = options.account ?? (realm.link ? realm.link(name) : undefined); if (!userAccount) { throw new AlephaError( "Authentication requires a link function in the realm primitive.", ); } const account: LinkAccountFn = async (opts) => { return userAccount(normalizeApplePayload(opts)); }; return $auth({ issuer: realm, name, oidc: { issuer: "https://appleid.apple.com", clientId: env.APPLE_CLIENT_ID!, clientSecret: env.APPLE_CLIENT_SECRET, scope: "name email", responseMode: "form_post", ...options, account, }, disabled, }); }; /** * Normalize Apple-specific profile quirks before handing off to the * user-provided link function. * * Why: Apple's ID token non-conformities — `email_verified` and * `is_private_email` are delivered as the strings "true"/"false" rather than * booleans. Normalize so downstream code can rely on standard OIDC shapes. */ const normalizeApplePayload = ( opts: LinkAccountOptions, ): LinkAccountOptions => { const user: OAuth2Profile = { ...opts.user }; for (const key of ["email_verified", "is_private_email"] as const) { const raw = user[key] as unknown; if (typeof raw === "string") { user[key] = raw === "true"; } } return { ...opts, user }; };