fix issues in backend implementions,
auth endpoint now fetches all relevant backend tokens
This commit is contained in:
parent
1a8e804be1
commit
a31d5a3336
@ -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": {
|
||||||
|
@ -35,16 +35,33 @@ 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.
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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.` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user