From eb70a14c6125e9d67123952d6166f020410b9362 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:12:43 +0100 Subject: [PATCH 01/14] Add routes for admin authentication endpoints + Add router to the app --- src/Routes/admin_auth.routes.ts | 8 ++++++++ src/app.ts | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 src/Routes/admin_auth.routes.ts diff --git a/src/Routes/admin_auth.routes.ts b/src/Routes/admin_auth.routes.ts new file mode 100644 index 0000000..e9408d3 --- /dev/null +++ b/src/Routes/admin_auth.routes.ts @@ -0,0 +1,8 @@ +import express from "express"; +import { authenticateUser } from "../Controllers/admin_auth.controller"; + +const router = express.Router(); + +router.post("/", authenticateUser); + +export default router; diff --git a/src/app.ts b/src/app.ts index 84e6cee..7507b63 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import express from "express"; import prisma from "./lib/prisma"; import eventRouter from "./Routes/event.routes"; +import adminAuthRouter from "./Routes/admin_auth.routes"; require("dotenv").config(); // Load dotenv config @@ -13,6 +14,9 @@ async function main() { app.use(express.urlencoded({ extended: true })); app.use(express.json()); + // Admin authentication endpoints + app.use("/api/authentication", adminAuthRouter); + // All API endpoints are behind /api/... app.use("/api/events", eventRouter); From 59f4d53af5ba2c08714bba7282ff8648cf90960c Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:37:28 +0100 Subject: [PATCH 02/14] Add a controller for getting all admin user details + Requires Authentication --- src/Controllers/admin.controller.ts | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Controllers/admin.controller.ts diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts new file mode 100644 index 0000000..bdd64d0 --- /dev/null +++ b/src/Controllers/admin.controller.ts @@ -0,0 +1,40 @@ +import prisma from "../lib/prisma"; +import { Request, Response } from "express"; + +// requires: auth(elevated) +export const getAllAdmins = async (req: Request, res: Response) => { + if (!req.auth?.isAuthenticated) { + return res.status(500).json({ + type: "failure", + payload: { + message: "The server was not able to validate your credentials; Please try again later", + }, + }); + } + + if (req.auth.permission_level !== "ELEVATED") { + return res.status(403).json({ + type: "error", + payload: { + message: "You do not have sufficient permissions to use this feature", + }, + _links: [ + { + rel: "authentication", + href: "/api/authentication", + type: "POST", + }, + ], + }); + } + + // TODO: Add exception handling + const users = await prisma.admin.findMany({ select: { pid: true, name: true, permission_level: true } }); + + res.status(200).json({ + type: "success", + payload: { + users, + }, + }); +}; From 913e0ee0e05b4d4e7d5fdffe41926062980939e2 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 3 Dec 2021 13:15:50 +0100 Subject: [PATCH 03/14] Remove event <-> admin association and introduce a default value for revision --- prisma/schema.prisma | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cabd3e2..0a4bcbe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,6 @@ model Event { description String disciplines Discipline[] - admins Admin[] organisations Organisation[] campaign Campaign[] } @@ -47,14 +46,11 @@ model Link { model Admin { id Int @id @default(autoincrement()) pid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String + name String // TODO: This should maybe be unique password String // TODO: Probably specify hash size (VarChar or some other type) permission_level AdminLevel @default(STANDARD) - revision String + revision String @default(dbgenerated("gen_random_uuid()")) @db.Uuid groups Group[] - - event Event @relation(fields: [eventId], references: [id]) - eventId Int } model Discipline { @@ -143,4 +139,4 @@ enum Gender { MALE FEMALE OTHER -} \ No newline at end of file +} From b82a84322b2fced9d5a70811942e1d97b7436c2c Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 20 Dec 2021 08:48:04 +0100 Subject: [PATCH 04/14] Extract code for error messages for better reusability + Create function for generating permission errors --- src/Controllers/admin.controller.ts | 44 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index bdd64d0..083d3c9 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -1,31 +1,37 @@ import prisma from "../lib/prisma"; import { Request, Response } from "express"; +import { AdminLevel } from "@prisma/client"; + +const AUTH_ERROR = { + type: "failure", + payload: { + message: "The server was not able to validate your credentials; Please try again later", + }, +}; + +const createInsufficientPermissionsError = (required: AdminLevel = "ELEVATED") => ({ + type: "error", + payload: { + message: "You do not have sufficient permissions to use this feature", + required_level: required, + }, + _links: [ + { + rel: "authentication", + href: "/api/authentication", + type: "POST", + }, + ], +}); // requires: auth(elevated) export const getAllAdmins = async (req: Request, res: Response) => { if (!req.auth?.isAuthenticated) { - return res.status(500).json({ - type: "failure", - payload: { - message: "The server was not able to validate your credentials; Please try again later", - }, - }); + return res.status(500).json(AUTH_ERROR); } if (req.auth.permission_level !== "ELEVATED") { - return res.status(403).json({ - type: "error", - payload: { - message: "You do not have sufficient permissions to use this feature", - }, - _links: [ - { - rel: "authentication", - href: "/api/authentication", - type: "POST", - }, - ], - }); + return res.status(403).json(createInsufficientPermissionsError()); } // TODO: Add exception handling From b57315d916c647f46de3545a1116c9fda428eba2 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 20 Dec 2021 09:06:05 +0100 Subject: [PATCH 05/14] Add controller for adding a user + Add function to narrow type of string to AdminLevel --- src/Controllers/admin.controller.ts | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 083d3c9..526d80f 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -1,6 +1,7 @@ import prisma from "../lib/prisma"; import { Request, Response } from "express"; import { AdminLevel } from "@prisma/client"; +import argon2 from "argon2"; const AUTH_ERROR = { type: "failure", @@ -44,3 +45,58 @@ export const getAllAdmins = async (req: Request, res: Response) => { }, }); }; + +interface CreateAdminBody { + name?: string; + password: string; + permission_level: string; + // TODO: Add groups or events +} + +const PERMISSION_LEVELS: readonly AdminLevel[] = ["ELEVATED", "STANDARD"]; // TODO: Enforce completeness + +const isPermissionLevel = (level: string): level is AdminLevel => PERMISSION_LEVELS.includes(level as any); + +// requires: auth(elevated) +export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Response) => { + if (!req.auth?.isAuthenticated) { + return res.status(500).json(AUTH_ERROR); + } + + if (req.auth.permission_level !== "ELEVATED") { + return res.status(403).json(createInsufficientPermissionsError()); + } + + const { name, password, permission_level } = req.body || {}; + + if (!(typeof name === "string" && typeof password == "string" && isPermissionLevel(permission_level))) { + return res.status(400).json({ + type: "error", + payload: { + message: "The body of your request did not conform to the requirements", + schema: { + body: { + name: "string", + password_level: "string", + permission_level: "'STANDARD' | 'ELEVATED'", + }, + }, + }, + }); + } + + const password_hash = await argon2.hash(password, { type: argon2.argon2id }); + + // TODO: Check for uniqueness of the name + const user = await prisma.admin.create({ + data: { name, password: password_hash, permission_level }, + select: { pid: true, name: true, permission_level: true }, + }); + + res.status(201).json({ + type: "success", + payload: { + user, + }, + }); +}; From 9b2c8b17c61acbd2510146961b80c375b702910a Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 20 Dec 2021 09:11:46 +0100 Subject: [PATCH 06/14] Add router for the /admin endpoint --- src/Routes/admin.routes.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Routes/admin.routes.ts diff --git a/src/Routes/admin.routes.ts b/src/Routes/admin.routes.ts new file mode 100644 index 0000000..49fe551 --- /dev/null +++ b/src/Routes/admin.routes.ts @@ -0,0 +1,11 @@ +import express from "express"; +import { getAllAdmins, createAdmin } from "../Controllers/admin.controller"; +import { requireAuthentication } from "../Middleware/auth/auth"; + +const router = express.Router(); + +router.get("/", requireAuthentication, getAllAdmins); + +router.post("/", requireAuthentication, createAdmin); + +export default router; From 3363039f79bb63087fc2a07afe87df65d13bc0d6 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 20 Dec 2021 10:12:06 +0100 Subject: [PATCH 07/14] Add code that ensures that an (admin) user is always present --- src/app.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app.ts b/src/app.ts index 7507b63..ce9c33f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,12 +2,20 @@ import express from "express"; import prisma from "./lib/prisma"; import eventRouter from "./Routes/event.routes"; import adminAuthRouter from "./Routes/admin_auth.routes"; +import argon2 from "argon2"; require("dotenv").config(); // Load dotenv config const app = express(); async function main() { + // Dev + await prisma.admin.upsert({ + where: { id: 1 }, + create: { name: "admin", password: await argon2.hash("test", { type: argon2.argon2id }) }, + update: {}, + }); + // Todo: Everything // Bodyparser and urlencoded to parse post request bodies From 97fee47c6e27eefd900e4eacf9f87c9bc5dfacfb Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Tue, 21 Dec 2021 12:19:17 +0100 Subject: [PATCH 08/14] Allow for groups to be specified when POSTing /api/admins + Add the admin router to the global app --- src/Controllers/admin.controller.ts | 29 +++++++++++++++++++++++++---- src/app.ts | 3 +++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 526d80f..d15d720 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -50,7 +50,7 @@ interface CreateAdminBody { name?: string; password: string; permission_level: string; - // TODO: Add groups or events + groups?: string[]; } const PERMISSION_LEVELS: readonly AdminLevel[] = ["ELEVATED", "STANDARD"]; // TODO: Enforce completeness @@ -67,9 +67,13 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re return res.status(403).json(createInsufficientPermissionsError()); } - const { name, password, permission_level } = req.body || {}; + const { name, password, permission_level, groups } = req.body || {}; - if (!(typeof name === "string" && typeof password == "string" && isPermissionLevel(permission_level))) { + const groupsIsValid = groups ? groups.filter((e) => typeof e !== "string").length === 0 : true; + + if ( + !(typeof name === "string" && typeof password == "string" && isPermissionLevel(permission_level) && groupsIsValid) + ) { return res.status(400).json({ type: "error", payload: { @@ -87,9 +91,26 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re const password_hash = await argon2.hash(password, { type: argon2.argon2id }); + // Check if all gropus exist + for (const groupId of groups || []) { + if (!(await prisma.group.findUnique({ where: { pid: groupId } }))) { + return res.status(404).json({ + type: "error", + payload: { + message: `The group with ID '${groupId}' could not be found!`, + }, + }); + } + } + // TODO: Check for uniqueness of the name const user = await prisma.admin.create({ - data: { name, password: password_hash, permission_level }, + data: { + name, + password: password_hash, + permission_level, + groups: { connect: groups?.map((group) => ({ pid: group })) }, + }, select: { pid: true, name: true, permission_level: true }, }); diff --git a/src/app.ts b/src/app.ts index ce9c33f..75f3c95 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import prisma from "./lib/prisma"; import eventRouter from "./Routes/event.routes"; import adminAuthRouter from "./Routes/admin_auth.routes"; import argon2 from "argon2"; +import adminRouter from "./Routes/admin.routes"; require("dotenv").config(); // Load dotenv config @@ -28,6 +29,8 @@ async function main() { // All API endpoints are behind /api/... app.use("/api/events", eventRouter); + app.use("/api/admins", adminRouter); + app.listen(process.env.PORT, () => { console.log(`Listening on Port: ${process.env.PORT}`); }); From bfca7e8672adaba09f091efc1e44fa44872652d3 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:16:29 +0100 Subject: [PATCH 09/14] Add the permission_level 'ELEVATED' to the default user --- src/app.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 75f3c95..deffe8a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,7 +13,11 @@ async function main() { // Dev await prisma.admin.upsert({ where: { id: 1 }, - create: { name: "admin", password: await argon2.hash("test", { type: argon2.argon2id }) }, + create: { + name: "admin", + password: await argon2.hash("test", { type: argon2.argon2id }), + permission_level: "ELEVATED", + }, update: {}, }); From 5b02905d7169a0a1b272fadf22aa178d084f4dbe Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Fri, 31 Dec 2021 00:11:04 +0100 Subject: [PATCH 10/14] Add a controller and router to update a user's password + Add a function to update the password internally + The current interface could be improved (made more RESTful) --- src/Controllers/admin.controller.ts | 92 +++++++++++++++++++++++++++++ src/Routes/admin.routes.ts | 5 +- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index d15d720..46ee403 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -121,3 +121,95 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re }, }); }; + +interface UpdatePasswordBody { + name?: string; + password?: string; + new_password: string; +} + +// Expects a valid username (Should be tested beforehand) +const updatePasswordField = async (name: string, new_password: string) => { + const new_password_hash = await argon2.hash(new_password, { type: argon2.argon2id }); + + await prisma.admin.updateMany({ where: { name }, data: { password: new_password_hash } }); +}; + +// requires: auth +export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, res: Response) => { + if (!req.auth?.isAuthenticated) { + return res.status(500).json(AUTH_ERROR); + } + + const name = req.body.name || req.auth.name; + + if (typeof req.body.new_password !== "string") { + return res.status(400).json({ + type: "error", + payload: { + message: "The body of your request did not conform to the requirements", + schema: { + body: { + new_password: "string", + }, + }, + }, + }); + } + + const user_to_upate = await prisma.admin.findFirst({ where: { name } }); + + if (!user_to_upate) { + // REVIEW: This allows potential attackers (which are authorized with some account) + // to test account names + return res.status(404).json({ + type: "error", + payload: { + message: "The requested user was not found", + }, + }); + } + + if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") { + updatePasswordField(name, req.body.new_password); + + res.status(200).json({ + type: "success", + }); + } else if (req.auth.name === name) { + if (typeof req.body.password !== "string") { + return res.status(400).json({ + type: "error", + payload: "The body of your request did not conform to the requirements", + schema: { + body: { + password: "string", + new_password: "string", + }, + }, + }); + } + + if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) { + updatePasswordField(name, req.body.new_password); + + return res.status(200).json({ + type: "success", + }); + } + + return res.status(401).json({ + type: "error", + payload: { + message: "The provided password is not valid", + }, + }); + } else { + res.status(403).json({ + type: "error", + payload: { + message: "Operation not permitted; Try logging in as another user", + }, + }); + } +}; diff --git a/src/Routes/admin.routes.ts b/src/Routes/admin.routes.ts index 49fe551..20d5df7 100644 --- a/src/Routes/admin.routes.ts +++ b/src/Routes/admin.routes.ts @@ -1,5 +1,5 @@ import express from "express"; -import { getAllAdmins, createAdmin } from "../Controllers/admin.controller"; +import { getAllAdmins, createAdmin, updatePassword } from "../Controllers/admin.controller"; import { requireAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); @@ -8,4 +8,7 @@ router.get("/", requireAuthentication, getAllAdmins); router.post("/", requireAuthentication, createAdmin); +// TODO: Use URL parameters to specify the user to update +router.put("/password", requireAuthentication, updatePassword); + export default router; From cc1c394dcecb922bcc7f210e15ec68dbf9f6b2be Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 2 Jan 2022 15:46:47 +0100 Subject: [PATCH 11/14] Change the controller for changing passwords to use PIDs and not usernames --- src/Controllers/admin.controller.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 46ee403..9cf735c 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -123,16 +123,16 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re }; interface UpdatePasswordBody { - name?: string; + pid?: string; password?: string; new_password: string; } // Expects a valid username (Should be tested beforehand) -const updatePasswordField = async (name: string, new_password: string) => { +const updatePasswordField = async (pid: string, new_password: string) => { const new_password_hash = await argon2.hash(new_password, { type: argon2.argon2id }); - await prisma.admin.updateMany({ where: { name }, data: { password: new_password_hash } }); + await prisma.admin.update({ where: { pid }, data: { password: new_password_hash } }); }; // requires: auth @@ -141,7 +141,7 @@ export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, r return res.status(500).json(AUTH_ERROR); } - const name = req.body.name || req.auth.name; + const pid = req.body.pid || req.auth.pid; if (typeof req.body.new_password !== "string") { return res.status(400).json({ @@ -157,7 +157,7 @@ export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, r }); } - const user_to_upate = await prisma.admin.findFirst({ where: { name } }); + const user_to_upate = await prisma.admin.findUnique({ where: { pid } }); if (!user_to_upate) { // REVIEW: This allows potential attackers (which are authorized with some account) @@ -171,12 +171,12 @@ export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, r } if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") { - updatePasswordField(name, req.body.new_password); + updatePasswordField(pid, req.body.new_password); res.status(200).json({ type: "success", }); - } else if (req.auth.name === name) { + } else if (req.auth.pid === pid) { if (typeof req.body.password !== "string") { return res.status(400).json({ type: "error", @@ -191,7 +191,7 @@ export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, r } if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) { - updatePasswordField(name, req.body.new_password); + updatePasswordField(pid, req.body.new_password); return res.status(200).json({ type: "success", From 4d23eccba64f3d64ffbcbe1f3a36ca7ac6ca8146 Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 2 Jan 2022 16:49:01 +0100 Subject: [PATCH 12/14] Restructure the process of changing a password + Requests are now dispatched to the according user ressource, not the collection + The current user can be referenced by setting the pid to 'current' + Refactored the error logic --- src/Controllers/admin.controller.ts | 119 +++++++++++++++------------- src/Controllers/common.ts | 20 +++++ src/Routes/admin.routes.ts | 6 +- 3 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 src/Controllers/common.ts diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 9cf735c..1cd66a1 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -2,6 +2,7 @@ import prisma from "../lib/prisma"; import { Request, Response } from "express"; import { AdminLevel } from "@prisma/client"; import argon2 from "argon2"; +import { DataType, generateInvalidBodyError } from "./common"; const AUTH_ERROR = { type: "failure", @@ -122,12 +123,6 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re }); }; -interface UpdatePasswordBody { - pid?: string; - password?: string; - new_password: string; -} - // Expects a valid username (Should be tested beforehand) const updatePasswordField = async (pid: string, new_password: string) => { const new_password_hash = await argon2.hash(new_password, { type: argon2.argon2id }); @@ -135,28 +130,72 @@ const updatePasswordField = async (pid: string, new_password: string) => { await prisma.admin.update({ where: { pid }, data: { password: new_password_hash } }); }; -// requires: auth -export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, res: Response) => { +interface UpdateForeignPasswordBody { + new_password?: string; +} + +interface UpdateForeignPasswordQueryParams { + pid: string; +} + +export const updateForeignPassword = async ( + req: Request, + res: Response +) => { if (!req.auth?.isAuthenticated) { return res.status(500).json(AUTH_ERROR); } - const pid = req.body.pid || req.auth.pid; - if (typeof req.body.new_password !== "string") { - return res.status(400).json({ + return res.status(400).json(generateInvalidBodyError({ new_password: DataType.STRING })); + } + + const user_to_upate = await prisma.admin.findUnique({ where: { pid: req.params.pid } }); + + if (!user_to_upate) { + // REVIEW: This allows potential attackers (which are authorized with some account) + // to test account names + return res.status(404).json({ type: "error", payload: { - message: "The body of your request did not conform to the requirements", - schema: { - body: { - new_password: "string", - }, - }, + message: "The requested user was not found", }, }); } + if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") { + updatePasswordField(req.params.pid, req.body.new_password); + + res.status(200).json({ + type: "success", + }); + } else { + res.status(403).json({ + type: "error", + payload: { + message: "Operation not permitted; Try logging in as another user", + }, + }); + } +}; + +interface UpdatePasswordBody { + password?: string; + new_password?: string; +} + +// requires: auth +export const updateOwnPassword = async (req: Request<{}, {}, UpdatePasswordBody>, res: Response) => { + if (!req.auth?.isAuthenticated) { + return res.status(500).json(AUTH_ERROR); + } + + const pid = req.auth.pid; + + if (typeof req.body.password !== "string" || typeof req.body.new_password !== "string") { + return res.status(400).json(generateInvalidBodyError({ password: DataType.STRING, new_password: DataType.STRING })); + } + const user_to_upate = await prisma.admin.findUnique({ where: { pid } }); if (!user_to_upate) { @@ -170,46 +209,18 @@ export const updatePassword = async (req: Request<{}, {}, UpdatePasswordBody>, r }); } - if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") { + if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) { updatePasswordField(pid, req.body.new_password); - res.status(200).json({ + return res.status(200).json({ type: "success", }); - } else if (req.auth.pid === pid) { - if (typeof req.body.password !== "string") { - return res.status(400).json({ - type: "error", - payload: "The body of your request did not conform to the requirements", - schema: { - body: { - password: "string", - new_password: "string", - }, - }, - }); - } - - if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) { - updatePasswordField(pid, req.body.new_password); - - return res.status(200).json({ - type: "success", - }); - } - - return res.status(401).json({ - type: "error", - payload: { - message: "The provided password is not valid", - }, - }); - } else { - res.status(403).json({ - type: "error", - payload: { - message: "Operation not permitted; Try logging in as another user", - }, - }); } + + return res.status(401).json({ + type: "error", + payload: { + message: "The provided password is not valid", + }, + }); }; diff --git a/src/Controllers/common.ts b/src/Controllers/common.ts new file mode 100644 index 0000000..2adc291 --- /dev/null +++ b/src/Controllers/common.ts @@ -0,0 +1,20 @@ +export enum DataType { + STRING = "string", + NUMBER = "number", + INTEGER = "integer", + PERMISSION_LEVEL = "'ELEVATED' | 'STANDARD'", +} + +interface Body { + [k: string]: DataType; +} + +export function generateInvalidBodyError(body: Body) { + return { + type: "error", + payload: { + message: "The body of your request did not conform to the requirements", + schema: { body }, + }, + }; +} diff --git a/src/Routes/admin.routes.ts b/src/Routes/admin.routes.ts index 20d5df7..b4be6ce 100644 --- a/src/Routes/admin.routes.ts +++ b/src/Routes/admin.routes.ts @@ -1,5 +1,5 @@ import express from "express"; -import { getAllAdmins, createAdmin, updatePassword } from "../Controllers/admin.controller"; +import { getAllAdmins, createAdmin, updateOwnPassword, updateForeignPassword } from "../Controllers/admin.controller"; import { requireAuthentication } from "../Middleware/auth/auth"; const router = express.Router(); @@ -8,7 +8,7 @@ router.get("/", requireAuthentication, getAllAdmins); router.post("/", requireAuthentication, createAdmin); -// TODO: Use URL parameters to specify the user to update -router.put("/password", requireAuthentication, updatePassword); +router.put("/current/password", requireAuthentication, updateOwnPassword); +router.put<"/:pid/password", { pid: string }>("/:pid/password", requireAuthentication, updateForeignPassword); export default router; From 08419d581bb5b972215cd5904d95b18cb2e0c84b Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:26:20 +0100 Subject: [PATCH 13/14] Update the admin revision on password change + Update type of revision to DateTime --- prisma/schema.prisma | 2 +- src/Controllers/admin.controller.ts | 18 ++++++++++++++++-- src/Controllers/admin_auth.controller.ts | 4 ++-- src/Middleware/auth/auth.ts | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a4bcbe..7d830ce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,7 +49,7 @@ model Admin { name String // TODO: This should maybe be unique password String // TODO: Probably specify hash size (VarChar or some other type) permission_level AdminLevel @default(STANDARD) - revision String @default(dbgenerated("gen_random_uuid()")) @db.Uuid + revision DateTime @default(now()) groups Group[] } diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 1cd66a1..69dd28f 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -3,6 +3,18 @@ import { Request, Response } from "express"; import { AdminLevel } from "@prisma/client"; import argon2 from "argon2"; import { DataType, generateInvalidBodyError } from "./common"; +import { authClient } from "../lib/redis"; + +export const regenerateRevision = async (pid: string) => { + // TOOO: Add error handling + const { revision } = await prisma.admin.update({ + where: { pid }, + data: { revision: new Date() }, + select: { revision: true }, + }); + + await authClient.set(pid, revision.toISOString()); +}; const AUTH_ERROR = { type: "failure", @@ -164,7 +176,8 @@ export const updateForeignPassword = async ( } if (req.auth.permission_level == "ELEVATED" && user_to_upate.permission_level == "STANDARD") { - updatePasswordField(req.params.pid, req.body.new_password); + await updatePasswordField(req.params.pid, req.body.new_password); // REVIEW: Should this be awaited? + await regenerateRevision(req.params.pid); res.status(200).json({ type: "success", @@ -210,7 +223,8 @@ export const updateOwnPassword = async (req: Request<{}, {}, UpdatePasswordBody> } if (await argon2.verify(user_to_upate.password, req.body.password, { type: argon2.argon2id })) { - updatePasswordField(pid, req.body.new_password); + await updatePasswordField(pid, req.body.new_password); + await regenerateRevision(pid); return res.status(200).json({ type: "success", diff --git a/src/Controllers/admin_auth.controller.ts b/src/Controllers/admin_auth.controller.ts index 97161d9..8d1ec0e 100644 --- a/src/Controllers/admin_auth.controller.ts +++ b/src/Controllers/admin_auth.controller.ts @@ -21,7 +21,7 @@ function createAdminJWT(admin: Admin & { groups: Group[] }) { pid: admin.pid, name: admin.name, permission_level: admin.permission_level, - revision: admin.revision, + revision: admin.revision.toISOString(), groups: admin.groups.map((group) => group.pid), }; @@ -64,7 +64,7 @@ export const authenticateUser = async (req: Request<{}, {}, AuthenticateUserBody if (await argon2.verify(user.password, password, { type: argon2.argon2id })) { // Set password revision ID in redis - await authClient.set(user.pid, user.revision); + await authClient.set(user.pid, user.revision.toISOString()); return res.status(200).json({ type: "success", diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 37853b5..0b66952 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -62,7 +62,7 @@ export const requireAuthentication = async (req: Request, res: Response, next: N const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } }); if (user) { - db_revision = user.revision; + db_revision = user.revision.toISOString(); await authClient.set(pid, db_revision); } From 064497a4cabe02965c10eb86cd8a12a891bbffdc Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:36:21 +0100 Subject: [PATCH 14/14] Refactor the endpoints to share common code --- src/Controllers/admin.controller.ts | 20 +++++++------------- src/Controllers/admin_auth.controller.ts | 8 ++------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Controllers/admin.controller.ts b/src/Controllers/admin.controller.ts index 69dd28f..714d98c 100644 --- a/src/Controllers/admin.controller.ts +++ b/src/Controllers/admin.controller.ts @@ -87,19 +87,13 @@ export const createAdmin = async (req: Request<{}, {}, CreateAdminBody>, res: Re if ( !(typeof name === "string" && typeof password == "string" && isPermissionLevel(permission_level) && groupsIsValid) ) { - return res.status(400).json({ - type: "error", - payload: { - message: "The body of your request did not conform to the requirements", - schema: { - body: { - name: "string", - password_level: "string", - permission_level: "'STANDARD' | 'ELEVATED'", - }, - }, - }, - }); + return res.status(400).json( + generateInvalidBodyError({ + name: DataType.STRING, + password: DataType.STRING, + permission_level: DataType.PERMISSION_LEVEL, + }) + ); } const password_hash = await argon2.hash(password, { type: argon2.argon2id }); diff --git a/src/Controllers/admin_auth.controller.ts b/src/Controllers/admin_auth.controller.ts index 8d1ec0e..33ce37b 100644 --- a/src/Controllers/admin_auth.controller.ts +++ b/src/Controllers/admin_auth.controller.ts @@ -4,6 +4,7 @@ import { authClient } from "../lib/redis"; import prisma from "../lib/prisma"; import argon2 from "argon2"; import jwt from "jsonwebtoken"; +import { DataType, generateInvalidBodyError } from "./common"; const JWT_SECRET = process.env.JWT_SECRET || "secret"; const TOKEN_EXPIRY = "4 days"; @@ -39,12 +40,7 @@ export const authenticateUser = async (req: Request<{}, {}, AuthenticateUserBody const { name, password } = req.body || {}; if (name == null || password == null) { - return res.status(400).json({ - type: "error", - payload: { - message: "The body must contain 'name' and 'password' attributes of type string", - }, - }); + return res.status(400).json(generateInvalidBodyError({ name: DataType.STRING, password: DataType.STRING })); } const user = await prisma.admin.findFirst({ where: { name }, include: { groups: true } });