build out google auth flow
This commit is contained in:
parent
fa57527066
commit
8575f38db9
6 changed files with 103 additions and 12 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -33,4 +33,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
*.sqlite
|
# Tables
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# Key files
|
||||||
|
private.key
|
||||||
|
public.pem
|
||||||
10
bun.lock
10
bun.lock
|
|
@ -6,11 +6,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"openid-client": "^6.6.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.0.7",
|
"@types/node": "^24.0.7",
|
||||||
|
"@types/openid-client": "^3.7.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -81,6 +83,8 @@
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/node": "^24.0.7",
|
"@types/node": "^24.0.7",
|
||||||
|
"@types/openid-client": "^3.7.0",
|
||||||
"drizzle-kit": "^0.31.4"
|
"drizzle-kit": "^0.31.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -14,7 +15,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"drizzle-orm": "^0.44.2"
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"openid-client": "^6.6.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/index.ts",
|
"dev": "bun --hot src/index.ts",
|
||||||
|
|
|
||||||
56
src/auth/google/client.ts
Normal file
56
src/auth/google/client.ts
Normal 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
11
src/index.html
Normal 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>
|
||||||
27
src/index.ts
27
src/index.ts
|
|
@ -1,17 +1,19 @@
|
||||||
import { createUser, getUser } from "./db/util";
|
import { createUser, getUser } from "./db/util";
|
||||||
|
import landing from "./index.html";
|
||||||
|
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
port: Number(process.env.PORT) || 3000,
|
port: Number(process.env.PORT) || 3000,
|
||||||
routes: {
|
routes: {
|
||||||
"/": () => {
|
"/": landing,
|
||||||
return new Response("Welcome to the landing page!", { status: 200 });
|
"/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) => {
|
"/auth/:provider/callback": async (req) => {
|
||||||
const filePath = `./src/public${req.url.slice(7)}`;
|
const { callback } = await import(`./auth/${req.params.provider}/client`);
|
||||||
if (Bun.file(filePath).size) {
|
let claims = await callback(new URL(req.url));
|
||||||
return new Response(Bun.file(filePath));
|
return Response.json(claims);
|
||||||
} else {
|
|
||||||
return new Response("File not found", { status: 404 });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"/api/users/:email": async (req) => {
|
"/api/users/:email": async (req) => {
|
||||||
return Response.json(await getUser(req.params.email));
|
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.", {
|
return new Response("Page not found.", {
|
||||||
status: 404,
|
status: 404,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue