// 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 };