110 lines
4.3 KiB
JavaScript
110 lines
4.3 KiB
JavaScript
// 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) => `<li><code>${web.escapeHtml(s)}</code></li>`).join("");
|
|
return `<!doctype html>
|
|
<html lang="en"><head><meta charset="utf-8"><title>Authorize</title>
|
|
<style>
|
|
body { font-family: system-ui, -apple-system, sans-serif; margin: 2rem; max-width: 480px; }
|
|
h1 { font-size: 1.3rem; }
|
|
code { font-family: ui-monospace, Menlo, Consolas, monospace; }
|
|
button { padding: 0.4rem 0.9rem; cursor: pointer; margin-right: 0.5rem; }
|
|
</style>
|
|
</head><body>
|
|
<h1>Authorize ${web.escapeHtml(clientLabel || clientId)}</h1>
|
|
<p><code>${web.escapeHtml(clientId)}</code> is requesting access to your account:</p>
|
|
<ul>${items}</ul>
|
|
<form method="post" action="${web.escapeHtml(constants.IDP_BASE_PATH)}/interaction/${web.escapeHtml(uid)}/confirm">
|
|
<button name="allow" value="1">Allow</button>
|
|
<button name="deny" value="1">Deny</button>
|
|
</form>
|
|
</body></html>`;
|
|
};
|
|
|
|
|
|
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
|
|
};
|