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
|
||||
.DS_Store
|
||||
|
||||
*.sqlite
|
||||
# Tables
|
||||
*.sqlite
|
||||
|
||||
# Key files
|
||||
private.key
|
||||
public.pem
|
||||
10
bun.lock
10
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=="],
|
||||
|
|
|
|||
|
|
@ -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
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 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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue