fix issues in backend implementions,

auth endpoint now fetches all relevant backend tokens
This commit is contained in:
Arthur Lu 2024-01-17 20:21:55 +00:00
parent 1a8e804be1
commit a31d5a3336
6 changed files with 141 additions and 44 deletions

View File

@ -13,6 +13,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"set-cookie-parser": "^2.6.0",
"ws": "^8.13.0" "ws": "^8.13.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -35,17 +35,34 @@ export class BACKEND {
/** /**
* Opens a session with the backend and creates session tokens if needed * Opens a session with the backend and creates session tokens if needed
* @param {Object} credentials object containing username and password fields * @param {Object} credentials object containing username and password fields
* @returns {Object[]} list of session token objects with token name and value * @returns {Object} response like object with ok, status, and list of session token objects with token name and value
*/ */
openSession (credentials) {} openSession (credentials) {
return {
ok: true,
status: 200,
cookies: []
};
}
/** /**
* Closes an opened session with the backend if needed * Closes an opened session with the backend if needed
* @param {*} token list of session token objects with token name and value * @param {Object[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend
* @returns {Boolean} true if session was closed successfully, false otherwise * @returns {Boolean} true if session was closed successfully, false otherwise
*/ */
closeSesssion (tokens) {} closeSession (tokens) {
return {
ok: true,
status: 200
};
}
} }
/**
* Interface for proxmox api backends.
*/
export class PVE_BACKEND extends BACKEND {}
/** /**
* Interface for user database backends. * Interface for user database backends.
*/ */

View File

@ -35,14 +35,6 @@ export default class LocalDB extends DB_BACKEND {
writeFileSync(this.#path, JSON.stringify(this.#data)); writeFileSync(this.#path, JSON.stringify(this.#data));
} }
openSession (credentials) {
return [];
}
closeSesssion (tokens) {
return true;
}
addUser (username, config = null) { addUser (username, config = null) {
config = config || this.#defaultuser; config = config || this.#defaultuser;
this.#data.users[username] = config; this.#data.users[username] = config;

View File

@ -1,5 +1,6 @@
import axios from "axios"; import axios from "axios";
import { AUTH_BACKEND } from "./backends.js"; import { AUTH_BACKEND } from "./backends.js";
import * as setCookie from "set-cookie-parser";
export default class PAASLDAP extends AUTH_BACKEND { export default class PAASLDAP extends AUTH_BACKEND {
#url = null; #url = null;
@ -29,12 +30,13 @@ export default class PAASLDAP extends AUTH_BACKEND {
}; };
if (auth) { if (auth) {
content.data.binduser = auth.binduser; content.headers.PAASLDAPAuthTicket = auth.PAASLDAPAuthTicket;
content.data.bindpass = auth.bindpass;
} }
try { try {
return await axios.request(url, content); const result = await axios.request(url, content);
result.ok = result.status === 200;
return result;
} }
catch (error) { catch (error) {
error.ok = false; error.ok = false;
@ -46,8 +48,28 @@ export default class PAASLDAP extends AUTH_BACKEND {
} }
} }
async modUser (userid, attributes, params = null) { async openSession (credentials) {
const bind = { binduser: params.binduser, bindpass: params.bindpass }; const userRealm = credentials.username.split("@").at(-1);
return await this.#request(`/users/${userid}`, "POST", bind, attributes); const uid = credentials.username.replace(`@${userRealm}`, "");
const content = { uid, password: credentials.password };
const result = await this.#request("/ticket", "POST", null, content);
if (result.ok) {
const cookies = setCookie.parse(result.headers["set-cookie"]);
cookies.forEach((e) => {
e.expiresMSFromNow = e.expires - Date.now();
});
return {
ok: true,
status: result.status,
cookies
};
}
else {
return result;
}
}
async modUser (userid, attributes, ticket) {
return await this.#request(`/users/${userid}`, "POST", ticket, attributes);
} }
} }

View File

@ -1,16 +1,43 @@
import axios from "axios"; import axios from "axios";
import { PVE_BACKEND } from "./backends.js";
export default class PVE { export default class PVE extends PVE_BACKEND {
#pveAPIURL = null; #pveAPIURL = null;
#pveAPIToken = null; #pveAPIToken = null;
#pveRoot = null; #pveRoot = null;
constructor (config) { constructor (config) {
super();
this.#pveAPIURL = config.url; this.#pveAPIURL = config.url;
this.#pveAPIToken = config.token; this.#pveAPIToken = config.token;
this.#pveRoot = config.root; this.#pveRoot = config.root;
} }
async openSession (credentials) {
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
if (!(response.status === 200)) {
return response;
}
const ticket = response.data.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken;
return {
ok: true,
status: response.status,
cookies: [
{
name: "PVEAuthCookie",
value: ticket,
expiresMSFromNow: 2 * 60 * 60 * 1000
},
{
name: "CSRFPreventionToken",
value: csrftoken,
expiresMSFromNow: 2 * 60 * 60 * 1000
}
]
};
}
/** /**
* Send HTTP request to proxmox API. Allows requests to be made with user cookie credentials or an API token for controlled priviledge elevation. * Send HTTP request to proxmox API. Allows requests to be made with user cookie credentials or an API token for controlled priviledge elevation.
* @param {string} path HTTP path, prepended with the proxmox API base url. * @param {string} path HTTP path, prepended with the proxmox API base url.

View File

@ -17,6 +17,34 @@ router.get("/", async (req, res) => {
res.status(200).send({ auth: true }); res.status(200).send({ auth: true });
}); });
/**
* Fetches and consumes cookies from backends and avoids duplicate cookies from repeat backends. Also helps handle errors.
*/
class CookieFetcher {
#fetchedBackends = [];
#cookies = [];
async fetchBackends (backends, credentials) {
for (const backend of backends) {
if (this.#fetchedBackends.indexOf(backend) === -1) {
const response = await backend.openSession(credentials);
if (!response.ok) {
return false;
}
this.#cookies = this.#cookies.concat(response.cookies);
this.#fetchedBackends.push(backend);
}
else { // assume that a repeat backends should not be requested
continue;
}
}
return true;
}
exportCookies () {
return this.#cookies;
}
}
/** /**
* POST - safer ticket generation using proxmox authentication but adding HttpOnly * POST - safer ticket generation using proxmox authentication but adding HttpOnly
* request: * request:
@ -27,22 +55,29 @@ router.get("/", async (req, res) => {
* - 401: {auth: false} * - 401: {auth: false}
*/ */
router.post("/ticket", async (req, res) => { router.post("/ticket", async (req, res) => {
const body = JSON.parse(JSON.stringify(req.body)); const params = {
const response = await global.pve.requestPVE("/access/ticket", "POST", null, body); username: req.body.username,
if (!(response.status === 200)) { password: req.body.password
res.status(response.status).send({ auth: false }); };
res.end(); const domain = global.config.application.domain;
const userRealm = params.username.split("@").at(-1);
const backends = [global.pve, global.db];
if (userRealm in global.auth) {
backends.push(global.auth[userRealm]);
}
const cm = new CookieFetcher();
const success = await cm.fetchBackends(backends, params);
if (!success) {
res.status(401).send({ auth: false });
return; return;
} }
const domain = global.config.application.domain; const cookies = cm.exportCookies();
const ticket = response.data.data.ticket; for (const cookie of cookies) {
const csrftoken = response.data.data.CSRFPreventionToken; const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
const username = response.data.data.username; res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate });
const expire = new Date(Date.now() + (2 * 60 * 60 * 1000)); }
res.cookie("PVEAuthCookie", ticket, { domain, path: "/", httpOnly: true, secure: true, expires: expire }); res.cookie("username", params.username, { domain, path: "/", secure: true });
res.cookie("CSRFPreventionToken", csrftoken, { domain, path: "/", httpOnly: true, secure: true, expires: expire }); res.cookie("auth", 1, { domain, path: "/", secure: true });
res.cookie("username", username, { domain, path: "/", secure: true, expires: expire });
res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expire });
res.status(200).send({ auth: true }); res.status(200).send({ auth: true });
}); });
@ -52,12 +87,17 @@ router.post("/ticket", async (req, res) => {
* - 200: {auth: false} * - 200: {auth: false}
*/ */
router.delete("/ticket", async (req, res) => { router.delete("/ticket", async (req, res) => {
const expire = new Date(0); if (Object.keys(req.cookies).length === 0) {
res.status(200).send({ auth: false });
return;
}
const domain = global.config.application.domain; const domain = global.config.application.domain;
res.cookie("PVEAuthCookie", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); const expire = new Date(0);
res.cookie("CSRFPreventionToken", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); for (const cookie in req.cookies) {
res.cookie("username", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); res.cookie(cookie, "", { domain, path: "/", expires: expire });
res.cookie("auth", 0, { domain, path: "/", expires: expire }); }
await global.pve.closeSession(req.cookies);
await global.db.closeSession(req.cookies);
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
}); });
@ -80,13 +120,11 @@ router.post("/password", async (req, res) => {
}; };
const userRealm = params.username.split("@").at(-1); const userRealm = params.username.split("@").at(-1);
const domains = (await global.pve.requestPVE("/access/domains", "GET", { token: true })).data.data;
const realm = domains.find((e) => e.realm === userRealm);
const authHandlers = global.config.handlers.auth; const authHandlers = global.config.handlers.auth;
if (realm.type in authHandlers) { if (userRealm in authHandlers) {
const handler = authHandlers[realm.type]; const handler = authHandlers[userRealm];
const userID = params.username.replace(`@${realm.realm}`, ""); const userID = params.username.replace(`@${userRealm}`, "");
const newAttributes = { const newAttributes = {
userpassword: params.password userpassword: params.password
}; };
@ -103,6 +141,6 @@ router.post("/password", async (req, res) => {
} }
} }
else { else {
res.status(501).send({ error: `Auth type ${realm.type} not implemented yet.` }); res.status(501).send({ error: `Auth type ${userRealm} not implemented yet.` });
} }
}); });