alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
118 lines (104 loc) • 3.35 kB
text/typescript
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,
});
};