// Login + consent interaction handlers. oidc-provider redirects the browser here // (interactions.url) when it needs the user to authenticate or grant consent. // login - reuse Saltcorn's session; if not logged in, bounce to /auth/login // (returning via ?dest=). Authenticate EXISTING users only. // consent - render a consent screen listing the client + requested scopes; // the Allow/Deny form posts to .../confirm. const constants = require("../constants"); const web = require("../web"); const clients = require("../clients"); const { getProviderEntry } = require("./provider"); const renderConsent = (uid, clientLabel, clientId, scopes) => { const items = scopes.map((s) => `
  • ${web.escapeHtml(s)}
  • `).join(""); return ` Authorize

    Authorize ${web.escapeHtml(clientLabel || clientId)}

    ${web.escapeHtml(clientId)} is requesting access to your account:

    `; }; const interactionHandler = async (req, res) => { const entry = await getProviderEntry(req); const provider = entry.provider; let details; try { details = await provider.interactionDetails(req, res); } catch (e) { // eslint-disable-next-line no-console console.error(`[${constants.PLUGIN_NAME}] interactionDetails failed:`, e); res.status(400).type("text/plain").send("invalid or expired interaction"); return; } const promptName = details.prompt && details.prompt.name; if (promptName === "login") { if (req.user && req.user.id) { await provider.interactionFinished(req, res, { login: { accountId: String(req.user.id) } }, { mergeWithLastSubmission: false }); } else { const back = constants.IDP_BASE_PATH + "/interaction/" + details.uid; res.redirect("/auth/login?dest=" + encodeURIComponent(back)); } return; } if (promptName === "consent") { const client = await clients.getClient(details.params.client_id); const scopes = String(details.params.scope || "").split(" ").filter(Boolean); res.type("text/html").send(renderConsent(details.uid, client && client.label, details.params.client_id, scopes)); return; } res.status(400).type("text/plain").send("unsupported interaction prompt: " + promptName); }; const confirmHandler = async (req, res) => { const entry = await getProviderEntry(req); const provider = entry.provider; let details; try { details = await provider.interactionDetails(req, res); } catch (e) { // eslint-disable-next-line no-console console.error(`[${constants.PLUGIN_NAME}] interactionDetails (confirm) failed:`, e); res.status(400).type("text/plain").send("invalid or expired interaction"); return; } if (req.body && req.body.deny) { await provider.interactionFinished(req, res, { error: "access_denied", error_description: "User denied the request" }, { mergeWithLastSubmission: false }); return; } const grant = new provider.Grant({ accountId: details.session.accountId, clientId: details.params.client_id }); if (details.params.scope) { grant.addOIDCScope(details.params.scope); } const grantId = await grant.save(); await provider.interactionFinished(req, res, { consent: { grantId: grantId } }, { mergeWithLastSubmission: true }); }; const interactionRoutes = [ { url: constants.INTERACTION_PATH, method: "get", callback: interactionHandler, noCsrf: true }, { url: constants.INTERACTION_CONFIRM_PATH, method: "post", callback: confirmHandler, noCsrf: true } ]; module.exports = { interactionRoutes };