Add Google OAuth2 flow
Redirect to Google consent screen, exchange code for token, fetch userinfo, derive username from email, create KV session. Refs #11
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { Hono } from "hono";
|
||||
import type { Env } from "../types";
|
||||
import { createSession, setSessionCookie, type Session } from "./session";
|
||||
|
||||
const google = new Hono<{ Bindings: Env }>();
|
||||
|
||||
// ─── Google OAuth endpoints ─────────────────────────────────────
|
||||
|
||||
interface GoogleTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
id_token?: string;
|
||||
}
|
||||
|
||||
interface GoogleUser {
|
||||
sub: string;
|
||||
name: string;
|
||||
email: string;
|
||||
email_verified: boolean;
|
||||
picture: string;
|
||||
}
|
||||
|
||||
// Step 1: Redirect to Google authorization
|
||||
google.get("/auth/google", (c) => {
|
||||
const clientId = c.env.GOOGLE_CLIENT_ID;
|
||||
if (!clientId) {
|
||||
return c.json({ error: "Google OAuth not configured" }, 500);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
redirect_uri: `${new URL(c.req.url).origin}/auth/google/callback`,
|
||||
response_type: "code",
|
||||
scope: "openid email profile",
|
||||
access_type: "offline",
|
||||
state: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
return c.redirect(
|
||||
`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Step 2: Handle callback, exchange code for token, create session
|
||||
google.get("/auth/google/callback", async (c) => {
|
||||
const code = c.req.query("code");
|
||||
if (!code) {
|
||||
return c.redirect("/?error=no_code");
|
||||
}
|
||||
|
||||
const clientId = c.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = c.env.GOOGLE_CLIENT_SECRET;
|
||||
if (!clientId || !clientSecret) {
|
||||
return c.json({ error: "Google OAuth not configured" }, 500);
|
||||
}
|
||||
|
||||
// Exchange code for access token
|
||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: `${new URL(c.req.url).origin}/auth/google/callback`,
|
||||
grant_type: "authorization_code",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
return c.redirect("/?error=token_exchange_failed");
|
||||
}
|
||||
|
||||
const tokenData = (await tokenRes.json()) as GoogleTokenResponse;
|
||||
if (!tokenData.access_token) {
|
||||
return c.redirect("/?error=no_access_token");
|
||||
}
|
||||
|
||||
// Fetch user profile from userinfo endpoint
|
||||
const userRes = await fetch(
|
||||
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
||||
},
|
||||
);
|
||||
|
||||
if (!userRes.ok) {
|
||||
return c.redirect("/?error=user_fetch_failed");
|
||||
}
|
||||
|
||||
const user = (await userRes.json()) as GoogleUser;
|
||||
|
||||
if (!user.email) {
|
||||
return c.redirect("/?error=no_email");
|
||||
}
|
||||
|
||||
// Derive a username from email prefix
|
||||
const usernameBase = user.email
|
||||
.split("@")[0]
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, "")
|
||||
.slice(0, 20);
|
||||
const username = usernameBase.length >= 3
|
||||
? usernameBase
|
||||
: usernameBase.padEnd(3, "0");
|
||||
|
||||
// Create session
|
||||
const session: Session = {
|
||||
username,
|
||||
email: user.email,
|
||||
avatar_url: user.picture,
|
||||
provider: "google",
|
||||
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 { google };
|
||||
Reference in New Issue
Block a user