add "request key" mechanism to protect resources such as auth
This commit is contained in:
parent
0c6c88f7f5
commit
15d22f261c
4 changed files with 101 additions and 57 deletions
|
@ -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",
|
if (ok) {
|
||||||
valueName: "Superuser Key",
|
overlayStore.open("toast", { message: "You have been promoted to superuser" });
|
||||||
async onSubmit(value) {
|
} else {
|
||||||
const { ok } = await request("POST", apiRoute("users/self/promote"), true, {
|
overlayStore.open("toast", { message: "Failed to promote to superuser" });
|
||||||
key: value
|
}
|
||||||
});
|
|
||||||
if (ok) {
|
|
||||||
overlayStore.open("toast", { message: "You have been promoted to superuser" });
|
|
||||||
} else {
|
|
||||||
overlayStore.open("toast", { message: "Failed to promote to superuser" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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,43 +32,71 @@ 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) {
|
||||||
const options = {
|
return new Promise(async (resolve, reject) => {
|
||||||
method,
|
const options = {
|
||||||
};
|
method,
|
||||||
|
|
||||||
if (body) {
|
|
||||||
options.body = JSON.stringify(body);
|
|
||||||
options.headers = {
|
|
||||||
...options.headers || {},
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (auth) {
|
if (body) {
|
||||||
const token = getItem("auth:token");
|
options.body = JSON.stringify(body);
|
||||||
if (token) {
|
|
||||||
options.headers = {
|
options.headers = {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
"Authorization": `Bearer ${token}`
|
"Content-Type": "application/json"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (auth) {
|
||||||
const res = await compatibleFetch(endpoint, options);
|
const token = getItem("auth:token");
|
||||||
return {
|
if (token) {
|
||||||
success: true,
|
options.headers = {
|
||||||
json: res.status === 204 ? null : await res.json(),
|
...options.headers || {},
|
||||||
ok: res.ok,
|
"Authorization": `Bearer ${token}`
|
||||||
status: res.status
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
return {
|
try {
|
||||||
success: false,
|
const res = await compatibleFetch(endpoint, options);
|
||||||
json: null,
|
const json = res.status === 204 ? {} : await res.json();
|
||||||
ok: false,
|
|
||||||
status: null
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue