build out google auth flow

This commit is contained in:
Badtz 2025-07-02 11:41:58 -07:00
parent fa57527066
commit 8575f38db9
6 changed files with 103 additions and 12 deletions

5
.gitignore vendored
View file

@ -33,4 +33,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# Tables
*.sqlite
# Key files
private.key
public.pem

View file

@ -6,11 +6,13 @@
"dependencies": {
"better-sqlite3": "^12.2.0",
"drizzle-orm": "^0.44.2",
"openid-client": "^6.6.2",
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@types/node": "^24.0.7",
"@types/openid-client": "^3.7.0",
"drizzle-kit": "^0.31.4",
},
"peerDependencies": {
@ -81,6 +83,8 @@
"@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="],
"@types/openid-client": ["@types/openid-client@3.7.0", "", { "dependencies": { "openid-client": "*" } }, "sha512-hW+QbAeUyfUHABwUjUECOcv56Mf5tN29xK3GPN7wy3R3XfIQdkm37XELA4wbe2RNG9GYUpN8l2ytfoW05XmpgQ=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"better-sqlite3": ["better-sqlite3@12.2.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ=="],
@ -131,6 +135,8 @@
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@ -143,8 +149,12 @@
"node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
"oauth4webapi": ["oauth4webapi@3.5.5", "", {}, "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openid-client": ["openid-client@6.6.2", "", { "dependencies": { "jose": "^6.0.11", "oauth4webapi": "^3.5.4" } }, "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],

View file

@ -7,6 +7,7 @@
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@types/node": "^24.0.7",
"@types/openid-client": "^3.7.0",
"drizzle-kit": "^0.31.4"
},
"peerDependencies": {
@ -14,7 +15,8 @@
},
"dependencies": {
"better-sqlite3": "^12.2.0",
"drizzle-orm": "^0.44.2"
"drizzle-orm": "^0.44.2",
"openid-client": "^6.6.2"
},
"scripts": {
"dev": "bun --hot src/index.ts",

56
src/auth/google/client.ts Normal file
View file

@ -0,0 +1,56 @@
import * as client from "openid-client";
const GOOGLE_ISSUER = new URL("https://accounts.google.com");
let OIDC: client.Configuration;
let code_verifier: string;
let state: string;
// setup OIDC client
async function initOIDC() {
OIDC = await client.discovery(
GOOGLE_ISSUER,
process.env.GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!
);
}
// start the login flow
export async function login() {
if (!OIDC) await initOIDC();
code_verifier = client.randomPKCECodeVerifier();
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
state = client.randomState();
const params = {
redirect_uri:
process.env.GOOGLE_REDIRECT_URI ||
"http://localhost:3000/auth/google/callback",
scope: "openid email profile",
code_challenge,
code_challenge_method: "S256" as const,
state,
};
// return the redirect URL
return client.buildAuthorizationUrl(OIDC, params);
}
// handle the callback from google after login
export async function callback(url: URL) {
if (!OIDC) await initOIDC();
try {
const tokenSet = await client.authorizationCodeGrant(OIDC, url, {
pkceCodeVerifier: code_verifier,
expectedState: state,
});
const claims = tokenSet.claims();
return claims;
} catch (error) {
console.error("Token exchange failed:", error);
return JSON.stringify({ error: "Authentication failed" });
}
}

11
src/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<ul>
<li><a href="/auth/google/login">Login with Google</a></li>
</ul>
</body>
</html>

View file

@ -1,17 +1,19 @@
import { createUser, getUser } from "./db/util";
import landing from "./index.html";
Bun.serve({
port: Number(process.env.PORT) || 3000,
routes: {
"/": () => {
return new Response("Welcome to the landing page!", { status: 200 });
"/": landing,
"/auth/:provider/login": async (req) => {
const { login } = await import(`./auth/${req.params.provider}/client`);
const redirectUrl = await login();
return Response.redirect(redirectUrl.toString(), 302);
},
"/public/*": (req) => {
const filePath = `./src/public${req.url.slice(7)}`;
if (Bun.file(filePath).size) {
return new Response(Bun.file(filePath));
} else {
return new Response("File not found", { status: 404 });
}
"/auth/:provider/callback": async (req) => {
const { callback } = await import(`./auth/${req.params.provider}/client`);
let claims = await callback(new URL(req.url));
return Response.json(claims);
},
"/api/users/:email": async (req) => {
return Response.json(await getUser(req.params.email));
@ -24,7 +26,12 @@ Bun.serve({
);
},
},
fetch(req): Response {
websocket: {
open() {},
message() {},
close() {},
},
fetch(): Response {
return new Response("Page not found.", {
status: 404,
});