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
No known key found for this signature in database
GPG key ID: 7C52899193467641
4 changed files with 101 additions and 57 deletions

View file

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

View file

@ -1,4 +1,6 @@
import { getItem } from "./storage";
// TODO: circular dependency
import { overlayStore } from "./stores";
export function compatibleFetch(endpoint, options) {
if (window.fetch && typeof window.fetch === "function") {
@ -30,43 +32,71 @@ export function compatibleFetch(endpoint, options) {
}
}
export default async function(method, endpoint, auth=true, body=null) {
const options = {
method,
};
if (body) {
options.body = JSON.stringify(body);
options.headers = {
...options.headers || {},
"Content-Type": "application/json"
export default function doRequest(method, endpoint, auth=true, body=null, _keyEntryDepth=false) {
return new Promise(async (resolve, reject) => {
const options = {
method,
};
}
if (auth) {
const token = getItem("auth:token");
if (token) {
if (body) {
options.body = JSON.stringify(body);
options.headers = {
...options.headers || {},
"Authorization": `Bearer ${token}`
"Content-Type": "application/json"
};
}
}
try {
const res = await compatibleFetch(endpoint, options);
return {
success: true,
json: res.status === 204 ? null : await res.json(),
ok: res.ok,
status: res.status
if (auth) {
const token = getItem("auth:token");
if (token) {
options.headers = {
...options.headers || {},
"Authorization": `Bearer ${token}`
};
}
}
} catch (e) {
return {
success: false,
json: null,
ok: false,
status: null
try {
const res = await compatibleFetch(endpoint, options);
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,
json,
ok: res.ok,
status: res.status
});
}
});
return;
}
return resolve({
success: true,
json,
ok: res.ok,
status: res.status
});
} catch (e) {
return resolve({
success: false,
json: null,
ok: false,
status: null
});
}
}
});
}

View file

@ -4,7 +4,7 @@ export const errors = {
BAD_AUTH: { code: 6003, message: "Bad authentication" },
NOT_FOUND: { code: 6004, message: "Not found" },
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" },
FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" },
INTERNAL_ERROR: { code: 7003, message: "Internal server error" }

View file

@ -2,14 +2,13 @@ import { errors } from "../../../errors";
import { query } from "../../../database";
import express from "express";
import { body, validationResult } from "express-validator";
import { compare, hash } from "bcrypt";
import { compare, hash, hashSync } from "bcrypt";
import { authenticateRoute, signToken } from "../../../auth";
import { dispatch } from "../../../gateway";
import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype";
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(
"/register",
@ -20,6 +19,16 @@ router.post(
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);
if (!validationErrors.isEmpty()) {
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("password").isLength({ min: 8, max: 1000 }),
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);
if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS });
@ -85,17 +104,20 @@ router.get(
router.post(
"/self/promote",
authenticateRoute(),
body("key").isLength({ min: 1, max: 3000 }),
body("requestKey").isLength({ min: 1, max: 3000 }),
async (req, res) => {
if (!superuserKey) {
return res.status(403).json({ ...errors.FEATURE_DISABLED });
}
const validationErrors = validationResult(req);
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]);
if (!updateUserResult || updateUserResult.rowCount < 1) {
return res.status(500).json({
@ -105,7 +127,7 @@ router.post(
return res.status(200).json({});
}
return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY });
return res.status(403).json({ ...errors.BAD_REQUEST_KEY });
}
);