From 8575f38db91ff2f7c4a0c0fe35659e1fb2030bb7 Mon Sep 17 00:00:00 2001 From: Badtz Date: Wed, 2 Jul 2025 11:41:58 -0700 Subject: [PATCH] build out google auth flow --- .gitignore | 7 ++++- bun.lock | 10 +++++++ package.json | 4 ++- src/auth/google/client.ts | 56 +++++++++++++++++++++++++++++++++++++++ src/index.html | 11 ++++++++ src/index.ts | 27 ++++++++++++------- 6 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/auth/google/client.ts create mode 100644 src/index.html diff --git a/.gitignore b/.gitignore index f974287..f133745 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -*.sqlite \ No newline at end of file +# Tables +*.sqlite + +# Key files +private.key +public.pem \ No newline at end of file diff --git a/bun.lock b/bun.lock index b4e6370..42564a3 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index c3e2792..559f295 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/auth/google/client.ts b/src/auth/google/client.ts new file mode 100644 index 0000000..ca64b25 --- /dev/null +++ b/src/auth/google/client.ts @@ -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" }); + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..e2bf753 --- /dev/null +++ b/src/index.html @@ -0,0 +1,11 @@ + + + + Login + + + + + diff --git a/src/index.ts b/src/index.ts index e60fb9b..bf788cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, });