personal-oidc/client.ts
2026-08-03 09:40:46 -06:00

111 lines
3.1 KiB
TypeScript

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("/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);
}
});