173 lines · 6.9 KB
import { env } from "cloudflare:workers";
import { z } from "zod";
import { first, run } from "./db";
import { randomToken, timingSafeEqualStrings, toBase64 } from "./crypto";
import { pendingLoginCookie, requiresSecondFactor } from "./two-factor";
import { cookieAttributes, cookieName, cookieValue, createSessionCookie } from "./auth";
import { accountNameTaken, createUser } from "./accounts";
import { isReservedName } from "~/lib/reserved-names";

const STATE_TTL_SECONDS = 600;
const GITHUB_USER_AGENT = "gitfor-forge";
const MAX_NAME_SUFFIX = 1000;
const STATE_COOKIE_BASE = "gitfor_oauth";

function clearStateCookie(): string {
  return `${cookieName(STATE_COOKIE_BASE)}=; ${cookieAttributes(0)}`;
}

const tokenResponse = z.object({ access_token: z.string() });
const githubUser = z.object({
  id: z.number(),
  login: z.string(),
  name: z.string().nullable(),
});
const githubEmails = z.array(z.object({ email: z.string(), primary: z.boolean(), verified: z.boolean() }));

function callbackUrl(request: Request): string {
  return new URL("/auth/github/callback", request.url).toString();
}

async function pkceChallenge(verifier: string): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
  return toBase64(new Uint8Array(digest)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
}

export async function startGithubLogin(request: Request): Promise<Response> {
  if (!env.GITHUB_CLIENT_ID) return new Response("GitHub login is not configured", { status: 404 });
  const state = randomToken("oauth");
  const verifier = randomToken("pkce");
  await env.KV.put(`oauth:${state}`, verifier, {