import express from "express"; import * as dotenv from "dotenv"; import * as oid from "openid-client"; import cookieParser from "cookie-parser"; import { uid } from "uid"; import jwt, { type JwtPayload } from "jsonwebtoken"; import assert from "assert"; dotenv.config({ quiet: true }); const AUTH_CLIENT_PORT = Number(process.env.AUTH_CLIENT_PORT) || 3002; const CLIENT_URL = String(process.env.CLIENT_URL) || "http://localhost"; const AUTHENTICATED_URL = String(process.env.AUTHENTICATED_URL) || "http://localhost"; const CLIENT_SECRET = String(process.env.CLIENT_SECRET); assert(CLIENT_SECRET.length > 0); const app = express(); const database: Record< string, { oidConfig: oid.Configuration; pkceCodeVerifier: string } > = {}; app.get("/login", async (req, res) => { if (typeof req.query.provider != "string") { res.status(400); res.end(); return; } const cbid = uid(); const redirect_uri = `${CLIENT_URL}/callback/${cbid}`; const provider = new URL(req.query.provider); const oidConfig = await oid.dynamicClientRegistration( provider, { redirect_uris: [redirect_uri], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "client_secret_basic", }, undefined, { execute: [oid.allowInsecureRequests] }, ); const pkceCodeVerifier = oid.randomPKCECodeVerifier(); const code_challenge = await oid.calculatePKCECodeChallenge(pkceCodeVerifier); database[cbid] = { oidConfig, pkceCodeVerifier }; setTimeout(() => { delete database[cbid]; }, 600000 /* 10 minutes */); res.redirect( oid.buildAuthorizationUrl(oidConfig, { redirect_uri, scope: "openid", code_challenge, code_challenge_method: "S256", }).href, ); }); app.get("/callback/:cbid", async (req, res) => { const cbid = req.params.cbid as string; if (!(cbid in database)) { res.status(400); res.end(); return; } const { oidConfig, pkceCodeVerifier } = database[cbid]!; const fullUrl = `${req.protocol}://${req.get("host")}${req.url}`; const url = new URL(fullUrl); let tokens = await oid .authorizationCodeGrant(oidConfig, url, { pkceCodeVerifier, idTokenExpected: true, }) .catch((e) => { console.error(e); }); // res.cookie("auth_tokens", JSON.stringify(tokens)); const claims = tokens!.claims(); if (claims) { const cookie = jwt.sign({ tokens, claims }, CLIENT_SECRET); res.cookie("auth_cookie", cookie); } res.redirect(AUTHENTICATED_URL); }); app.use(cookieParser()); app.get("/noauth", (req, res) => { res.status(200); res.end(); }); app.get("/authorize", (req, res) => { try { const { tokens, claims } = jwt.verify( req.cookies.auth_cookie, CLIENT_SECRET, ) as JwtPayload; console.log(`Authorized '${claims.sub}' from ${claims.iss}`); res.setHeader("AuthSub", claims.sub); res.setHeader("AuthIssuer", claims.iss); } catch (e) { res.status(401); res.send("Not Authorized"); } res.end(); }); app.listen(AUTH_CLIENT_PORT, (err) => { if (!err) { console.log(`Auth client listening on port ${AUTH_CLIENT_PORT}`); } else { console.error(err); } });