UNPKG

alepha

Version:

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

118 lines (104 loc) 3.35 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 OidcOptions, type WithLinkFn, } from "./$auth.ts"; /** * Already configured GitHub authentication primitive. * * Uses OAuth2 to authenticate users via their GitHub accounts. * Upon successful authentication, it links the GitHub account to a user session. * * Environment Variables: * - `GITHUB_CLIENT_ID`: The client ID obtained from the GitHub Developer Settings. * - `GITHUB_CLIENT_SECRET`: The client secret obtained from the GitHub Developer Settings. */ export const $authGithub = ( realm: IssuerPrimitive & WithLinkFn, options: Partial<OidcOptions> = {}, ) => { const { alepha } = $context(); const env = alepha.parseEnv( z.object({ GITHUB_CLIENT_ID: z .text({ description: "The OAuth App client ID obtained from GitHub Developer Settings.", }) .optional(), GITHUB_CLIENT_SECRET: z .text({ description: "The OAuth App client secret obtained from GitHub Developer Settings.", }) .optional(), }), ); const disabled = !env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET; const name = "github"; const account: LinkAccountFn | undefined = options.account ?? (realm.link ? realm.link(name) : undefined); if (!account) { throw new AlephaError( "Authentication requires a link function in the realm primitive.", ); } return $auth({ issuer: realm, name, oauth: { clientId: env.GITHUB_CLIENT_ID!, clientSecret: env.GITHUB_CLIENT_SECRET!, authorization: "https://github.com/login/oauth/authorize", token: "https://github.com/login/oauth/access_token", scope: "read:user user:email", userinfo: async (tokens) => { const BASE_URL = "https://api.github.com"; const headers = { Authorization: `Bearer ${tokens.access_token}`, "User-Agent": "Alepha", }; const res = await fetch(`${BASE_URL}/user`, { headers }).then((res) => res.json(), ); const user: OAuth2Profile = { sub: res.id.toString(), }; if (res.email) { user.email = res.email; } if (res.name) { user.name = res.name.trim(); } if (res.avatar_url) { user.picture = res.avatar_url; } // `/user` omits the email if the user's public profile hides it, and // never exposes `verified`. Fetch `/user/emails` to fill in both. const emailsRes = await fetch(`${BASE_URL}/user/emails`, { headers }); if (emailsRes.ok) { const emails: Array<{ email: string; primary: boolean; verified: boolean; }> = await emailsRes.json(); if (!user.email) { user.email = (emails.find((e) => e.primary) ?? emails[0])?.email; } if (user.email) { user.email_verified = emails.find((e) => e.email === user.email)?.verified ?? false; } } return user; }, ...options, account, }, disabled, }); };