Add GitHub OAuth2 flow

Redirect to GitHub authorization, exchange code for token,
fetch user profile + email, create KV session.

Refs #11
This commit is contained in:
marauder-actual
2026-06-12 09:08:57 +02:00
parent ca09e19de7
commit 47d2d00d77
+145
View File
@@ -0,0 +1,145 @@
import { Hono } from "hono";
import type { Env } from "../types";
import { createSession, setSessionCookie, type Session } from "./session";
const github = new Hono<{ Bindings: Env }>();
// ─── GitHub OAuth endpoints ─────────────────────────────────────
interface GitHubTokenResponse {
access_token: string;
token_type: string;
scope: string;
}
interface GitHubUser {
login: string;
avatar_url: string;
email: string | null;
name: string | null;
}
interface GitHubEmail {
email: string;
primary: boolean;
verified: boolean;
}
// Step 1: Redirect to GitHub authorization
github.get("/auth/github", (c) => {
const clientId = c.env.GITHUB_CLIENT_ID;
if (!clientId) {
return c.json({ error: "GitHub OAuth not configured" }, 500);
}
const state = crypto.randomUUID();
const params = new URLSearchParams({
client_id: clientId,
scope: "user:email",
state,
redirect_uri: `${new URL(c.req.url).origin}/auth/github/callback`,
});
return c.redirect(
`https://github.com/login/oauth/authorize?${params.toString()}`,
);
});
// Step 2: Handle callback, exchange code for token, create session
github.get("/auth/github/callback", async (c) => {
const code = c.req.query("code");
if (!code) {
return c.redirect("/?error=no_code");
}
const clientId = c.env.GITHUB_CLIENT_ID;
const clientSecret = c.env.GITHUB_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return c.json({ error: "GitHub OAuth not configured" }, 500);
}
// Exchange code for access token
const tokenRes = await fetch(
"https://github.com/login/oauth/access_token",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code,
}),
},
);
if (!tokenRes.ok) {
return c.redirect("/?error=token_exchange_failed");
}
const tokenData = (await tokenRes.json()) as GitHubTokenResponse;
if (!tokenData.access_token) {
return c.redirect("/?error=no_access_token");
}
// Fetch user profile
const userRes = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
Accept: "application/json",
"User-Agent": "madcat-provisioner",
},
});
if (!userRes.ok) {
return c.redirect("/?error=user_fetch_failed");
}
const user = (await userRes.json()) as GitHubUser;
// If no public email, fetch from emails API
let email = user.email;
if (!email) {
const emailsRes = await fetch("https://api.github.com/user/emails", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
Accept: "application/json",
"User-Agent": "madcat-provisioner",
},
});
if (emailsRes.ok) {
const emails = (await emailsRes.json()) as GitHubEmail[];
const primary = emails.find((e) => e.primary && e.verified);
email = primary?.email ?? emails[0]?.email ?? null;
}
}
if (!email) {
return c.redirect("/?error=no_email");
}
// Create session
const session: Session = {
username: user.login.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 20),
email,
avatar_url: user.avatar_url,
provider: "github",
created_at: new Date().toISOString(),
};
const sessionId = await createSession(c.env.INSTANCES, session);
const cookie = await setSessionCookie(sessionId, c.env.SESSION_SECRET);
return new Response(null, {
status: 302,
headers: {
Location: "/dashboard",
"Set-Cookie": cookie,
},
});
});
export { github };