sc-idp/lib/oidc/routes.js
2026-06-01 16:40:54 -05:00

89 lines
3.7 KiB
JavaScript

// OIDC endpoints served by oidc-provider, mounted under IDP_BASE_PATH via
// delegation. We strip the /idp prefix so oidc-provider's mount-relative router
// matches, keep req.originalUrl so it derives the mount path, and (for POST)
// re-stream the body that Saltcorn/Express already parsed, since oidc-provider
// reads the raw request stream.
const { Readable } = require("stream");
const constants = require("../constants");
const { getProviderEntry } = require("./provider");
// Saltcorn's body parser drains the POST stream, so rebuild a readable carrying
// the body (the exact rawBody if available, else re-encoded req.body) for
// oidc-provider's own parser. Copies the IncomingMessage props Koa/raw-body use,
// with the mount-relative url and a corrected content-length.
const restreamBody = (req, strippedUrl, fullUrl) => {
let buf;
let contentLength;
if (req.rawBody && req.rawBody.length) {
buf = req.rawBody;
contentLength = String(req.headers["content-length"] || buf.length);
} else if (req.body && typeof req.body === "object" && Object.keys(req.body).length) {
const ct = String(req.headers["content-type"] || "");
buf = ct.includes("application/json")
? Buffer.from(JSON.stringify(req.body))
: Buffer.from(new URLSearchParams(req.body).toString());
contentLength = String(buf.length);
} else {
buf = Buffer.alloc(0);
contentLength = "0";
}
const stream = new Readable({ read() {} });
stream.push(buf);
stream.push(null);
stream.headers = Object.assign({}, req.headers, { "content-length": contentLength });
stream.rawHeaders = req.rawHeaders;
stream.method = req.method;
stream.url = strippedUrl;
stream.originalUrl = fullUrl;
stream.httpVersion = req.httpVersion;
stream.httpVersionMajor = req.httpVersionMajor;
stream.httpVersionMinor = req.httpVersionMinor;
stream.socket = req.socket;
stream.connection = req.connection;
stream.complete = false;
return stream;
};
const delegate = async (req, res) => {
try {
const entry = await getProviderEntry(req);
const fullUrl = req.originalUrl || req.url;
const stripped = fullUrl.slice(constants.IDP_BASE_PATH.length) || "/";
let target = req;
if (req.method === "GET" || req.method === "HEAD") {
req.url = stripped;
} else {
target = restreamBody(req, stripped, fullUrl);
}
await entry.handler(target, res);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`[${constants.PLUGIN_NAME}] oidc delegate failed:`, e);
if (!res.headersSent) {
res.status(500).json({ error: "server_error" });
}
}
};
const oidcRoutes = [
{ url: constants.WELL_KNOWN_OPENID, method: "get", callback: delegate, noCsrf: true },
{ url: constants.JWKS_PATH, method: "get", callback: delegate, noCsrf: true },
{ url: constants.AUTH_PATH, method: "get", callback: delegate, noCsrf: true },
{ url: constants.AUTH_PATH, method: "post", callback: delegate, noCsrf: true },
{ url: constants.AUTH_RESUME_PATH, method: "get", callback: delegate, noCsrf: true },
{ url: constants.AUTH_RESUME_PATH, method: "post", callback: delegate, noCsrf: true },
{ url: constants.TOKEN_PATH, method: "post", callback: delegate, noCsrf: true },
{ url: constants.USERINFO_PATH, method: "get", callback: delegate, noCsrf: true },
{ url: constants.USERINFO_PATH, method: "post", callback: delegate, noCsrf: true }
];
module.exports = {
oidcRoutes
};