add "request key" mechanism to protect resources such as auth

This commit is contained in:
hippoz 2022-08-31 17:12:50 +03:00
parent 0c6c88f7f5
commit 15d22f261c
Signed by: hippoz
GPG key ID: 7C52899193467641
4 changed files with 101 additions and 57 deletions

View file

@ -16,21 +16,13 @@
}); });
}; };
const doSuperuserPrompt = () => { const doSuperuserPrompt = async () => {
overlayStore.open("prompt", { const { ok } = await request("POST", apiRoute("users/self/promote"), true);
heading: "Become Superuser",
valueName: "Superuser Key",
async onSubmit(value) {
const { ok } = await request("POST", apiRoute("users/self/promote"), true, {
key: value
});
if (ok) { if (ok) {
overlayStore.open("toast", { message: "You have been promoted to superuser" }); overlayStore.open("toast", { message: "You have been promoted to superuser" });
} else { } else {
overlayStore.open("toast", { message: "Failed to promote to superuser" }); overlayStore.open("toast", { message: "Failed to promote to superuser" });
} }
}
});
}; };
</script> </script>

View file

@ -1,4 +1,6 @@
import { getItem } from "./storage"; import { getItem } from "./storage";
// TODO: circular dependency
import { overlayStore } from "./stores";
export function compatibleFetch(endpoint, options) { export function compatibleFetch(endpoint, options) {
if (window.fetch && typeof window.fetch === "function") { if (window.fetch && typeof window.fetch === "function") {
@ -30,7 +32,8 @@ export function compatibleFetch(endpoint, options) {
} }
} }
export default async function(method, endpoint, auth=true, body=null) { export default function doRequest(method, endpoint, auth=true, body=null, _keyEntryDepth=false) {
return new Promise(async (resolve, reject) => {
const options = { const options = {
method, method,
}; };
@ -55,18 +58,45 @@ export default async function(method, endpoint, auth=true, body=null) {
try { try {
const res = await compatibleFetch(endpoint, options); const res = await compatibleFetch(endpoint, options);
return { const json = res.status === 204 ? {} : await res.json();
if (res.status === 403 && json.code && json.code === 6006 && !_keyEntryDepth) {
// This endpoint is password-protected
overlayStore.open("prompt", {
heading: "Enter Key For Resource",
valueName: "Key",
async onSubmit(value) {
const response = await doRequest(method, endpoint, auth, {
...(body || {}),
requestKey: value
}, true);
resolve(response);
},
onClose() {
resolve({
success: true, success: true,
json: res.status === 204 ? null : await res.json(), json,
ok: res.ok, ok: res.ok,
status: res.status status: res.status
});
} }
});
return;
}
return resolve({
success: true,
json,
ok: res.ok,
status: res.status
});
} catch (e) { } catch (e) {
return { return resolve({
success: false, success: false,
json: null, json: null,
ok: false, ok: false,
status: null status: null
});
} }
} });
} }

View file

@ -4,7 +4,7 @@ export const errors = {
BAD_AUTH: { code: 6003, message: "Bad authentication" }, BAD_AUTH: { code: 6003, message: "Bad authentication" },
NOT_FOUND: { code: 6004, message: "Not found" }, NOT_FOUND: { code: 6004, message: "Not found" },
FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" }, FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" },
BAD_SUPERUSER_KEY: { code: 6006, message: "Bad superuser key" }, BAD_REQUEST_KEY: { code: 6006, message: "Bad request key" },
GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" }, GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" },
FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" }, FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" },
INTERNAL_ERROR: { code: 7003, message: "Internal server error" } INTERNAL_ERROR: { code: 7003, message: "Internal server error" }

View file

@ -2,14 +2,13 @@ import { errors } from "../../../errors";
import { query } from "../../../database"; import { query } from "../../../database";
import express from "express"; import express from "express";
import { body, validationResult } from "express-validator"; import { body, validationResult } from "express-validator";
import { compare, hash } from "bcrypt"; import { compare, hash, hashSync } from "bcrypt";
import { authenticateRoute, signToken } from "../../../auth"; import { authenticateRoute, signToken } from "../../../auth";
import { dispatch } from "../../../gateway";
import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype";
const router = express.Router(); const router = express.Router();
const superuserKey = process.env.SUPERUSER_KEY || ""; const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null;
const authRequestKey = process.env.AUTH_REQUEST_KEY ? hashSync(process.env.AUTH_REQUEST_KEY, 10) : null;
router.post( router.post(
"/register", "/register",
@ -20,6 +19,16 @@ router.post(
return res.status(403).json({ ...errors.FEATURE_DISABLED }); return res.status(403).json({ ...errors.FEATURE_DISABLED });
} }
if (authRequestKey) {
if (!req.body.requestKey) {
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
}
const result = await compare(req.body.requestKey, authRequestKey);
if (!result) {
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
}
}
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
@ -52,6 +61,16 @@ router.post(
body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }),
body("password").isLength({ min: 8, max: 1000 }), body("password").isLength({ min: 8, max: 1000 }),
async (req, res) => { async (req, res) => {
if (authRequestKey) {
if (!req.body.requestKey) {
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
}
const result = await compare(req.body.requestKey, authRequestKey);
if (!result) {
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
}
}
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS }); return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS });
@ -85,17 +104,20 @@ router.get(
router.post( router.post(
"/self/promote", "/self/promote",
authenticateRoute(), authenticateRoute(),
body("key").isLength({ min: 1, max: 3000 }), body("requestKey").isLength({ min: 1, max: 3000 }),
async (req, res) => { async (req, res) => {
if (!superuserKey) {
return res.status(403).json({ ...errors.FEATURE_DISABLED });
}
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
} }
const { key } = req.body; const matches = await compare(req.body.requestKey, superuserKey);
if (superuserKey && superuserKey.length >= 1 && key === superuserKey && req.user) { if (matches) {
const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]); const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]);
if (!updateUserResult || updateUserResult.rowCount < 1) { if (!updateUserResult || updateUserResult.rowCount < 1) {
return res.status(500).json({ return res.status(500).json({
@ -105,7 +127,7 @@ router.post(
return res.status(200).json({}); return res.status(200).json({});
} }
return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
} }
); );