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