153 lines
4.4 KiB
JavaScript
153 lines
4.4 KiB
JavaScript
import crypto from "crypto";
|
|
import { Router } from "express";
|
|
import { Issuer, generators } from "openid-client";
|
|
import config from "../config.js";
|
|
import { getLogger } from "../logger.js";
|
|
|
|
const logger = getLogger("OIDC");
|
|
|
|
export const SESSION_COOKIE = "ta.sid";
|
|
const DEFAULT_APP_REDIRECT = "allotment://auth";
|
|
|
|
let client;
|
|
|
|
/** Same format as the express-session cookie value (`s:` + cookie-signature). */
|
|
function signSessionId(sessionId) {
|
|
const mac = crypto
|
|
.createHmac("sha256", config.auth.sessionSecret)
|
|
.update(sessionId)
|
|
.digest("base64")
|
|
.replace(/=+$/, "");
|
|
return `s:${sessionId}.${mac}`;
|
|
}
|
|
|
|
/**
|
|
* Lets the iOS app authenticate with `Authorization: Bearer <token>` by
|
|
* presenting the token from the app login redirect as the session cookie.
|
|
* Must run before the session middleware.
|
|
*/
|
|
export function bearerSessionMiddleware(req, _res, next) {
|
|
const match = /^Bearer\s+(s:.+)$/i.exec(req.headers.authorization || "");
|
|
if (match) {
|
|
req.headers.cookie = `${SESSION_COOKIE}=${encodeURIComponent(match[1].trim())}`;
|
|
}
|
|
next();
|
|
}
|
|
|
|
export async function createOidcClient() {
|
|
const {
|
|
issuer: issuerUrl,
|
|
clientId,
|
|
clientSecret,
|
|
redirectUri,
|
|
appCallbackUri,
|
|
} = config.auth.oidc;
|
|
|
|
if (!issuerUrl || !clientId || !redirectUri) {
|
|
logger.warn(
|
|
"OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI not set — OIDC disabled.",
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const issuer = await Issuer.discover(issuerUrl);
|
|
client = new issuer.Client({
|
|
client_id: clientId,
|
|
client_secret: clientSecret || undefined,
|
|
redirect_uris: appCallbackUri
|
|
? [redirectUri, appCallbackUri]
|
|
: [redirectUri],
|
|
response_types: ["code"],
|
|
});
|
|
return client;
|
|
}
|
|
|
|
export function createOidcRouter(clientUrl) {
|
|
const router = Router();
|
|
|
|
router.get("/auth/login", async (req, res, next) => {
|
|
if (!client) {
|
|
return res.status(503).send("OIDC not configured");
|
|
}
|
|
try {
|
|
const nonce = generators.nonce();
|
|
const state = generators.state();
|
|
const code_verifier = generators.codeVerifier();
|
|
const code_challenge = generators.codeChallenge(code_verifier);
|
|
const appLogin = req.query.app === "1";
|
|
const redirectUri =
|
|
(appLogin && config.auth.oidc.appCallbackUri) ||
|
|
config.auth.oidc.redirectUri;
|
|
req.session.oidc = { nonce, state, code_verifier, appLogin, redirectUri };
|
|
const authorizationUrl = client.authorizationUrl({
|
|
redirect_uri: redirectUri,
|
|
scope: "openid profile email",
|
|
state,
|
|
nonce,
|
|
code_challenge,
|
|
code_challenge_method: "S256",
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
req.session.save((err) => (err ? reject(err) : resolve()));
|
|
});
|
|
res.redirect(authorizationUrl);
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
router.get("/auth/callback", async (req, res, next) => {
|
|
if (!client) {
|
|
return res.status(503).send("OIDC not configured");
|
|
}
|
|
try {
|
|
const params = client.callbackParams(req);
|
|
const expectedState = req.session.oidc?.state;
|
|
const checks = {
|
|
state: expectedState,
|
|
nonce: req.session.oidc?.nonce,
|
|
code_verifier: req.session.oidc?.code_verifier,
|
|
};
|
|
const redirectUri =
|
|
req.session.oidc?.redirectUri || config.auth.oidc.redirectUri;
|
|
const tokenSet = await client.callback(redirectUri, params, checks);
|
|
const claims = tokenSet.claims();
|
|
req.session.user = {
|
|
sub: claims.sub,
|
|
email: claims.email,
|
|
name: claims.name,
|
|
};
|
|
const appLogin = req.session.oidc?.appLogin === true;
|
|
delete req.session.oidc;
|
|
await new Promise((resolve, reject) => {
|
|
req.session.save((err) => (err ? reject(err) : resolve()));
|
|
});
|
|
if (appLogin) {
|
|
const token = signSessionId(req.sessionID);
|
|
const target = new URL(config.auth.appRedirectUri || DEFAULT_APP_REDIRECT);
|
|
target.searchParams.set("token", token);
|
|
res.redirect(target.toString());
|
|
return;
|
|
}
|
|
res.redirect(clientUrl || "/");
|
|
} catch (e) {
|
|
logger.error("callback error:", e);
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
router.post("/auth/logout", (req, res) => {
|
|
req.session.destroy(() => {
|
|
res.json({ ok: true });
|
|
});
|
|
});
|
|
|
|
router.get("/auth/logout", (req, res) => {
|
|
req.session.destroy(() => {
|
|
res.redirect(clientUrl || "/");
|
|
});
|
|
});
|
|
|
|
return router;
|
|
}
|