From eb20ff4965a9a376d9e5317aa8c9f25924598cbb Mon Sep 17 00:00:00 2001 From: Stephan <57194608+stephan418@users.noreply.github.com> Date: Mon, 30 May 2022 18:18:44 +0200 Subject: [PATCH] Refractor auth logic + Add possibility (_requireAdminAuthentication, _requireTeamleaderAuthentication) to make auth optional + Add possibility to combine multiple auth types into one function --- dev.sh | 1 + src/Middleware/auth/auth.ts | 203 ++++++++++++++++++-------- src/Middleware/auth/teamleaderAuth.ts | 92 +++++++----- 3 files changed, 194 insertions(+), 102 deletions(-) diff --git a/dev.sh b/dev.sh index 7067090..dcd55c1 100755 --- a/dev.sh +++ b/dev.sh @@ -52,6 +52,7 @@ if [ "$RECREATE" = true ]; then -e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \ -e NODE_ENV="development" \ -e PORT="${D_PORT}" \ + -e JWT_SECRET="not_for_production" \ -p "${D_PORT}":"${D_PORT}" \ --entrypoint "/app/scripts/docker-entrypoint.dev.sh" \ node diff --git a/src/Middleware/auth/auth.ts b/src/Middleware/auth/auth.ts index 339b600..75e20c7 100644 --- a/src/Middleware/auth/auth.ts +++ b/src/Middleware/auth/auth.ts @@ -6,6 +6,9 @@ import { authClient } from "../../lib/redis"; import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; import prisma from "../../lib/prisma"; import AuthError from "../error/AuthError"; +import { TeamleaderJWTPayload, _requireTeamleaderAuthentication } from "./teamleaderAuth"; + +require("express-async-errors"); const JWT_SECRET = process.env.JWT_SECRET; @@ -13,38 +16,81 @@ export const verifyAuthorizationFormat = (authorization: string) => /^Bearer .+$ export const getBearerToken = (authorization: string) => authorization.slice(7); -export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => { - if (!JWT_SECRET) { - throw new Error("JWT_SECRET not set"); - } +const _requireAdminAuthentication = + (config: { optional?: Boolean; controlled?: Boolean } = { optional: false, controlled: false }) => + async (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } - const { authorization } = req.headers; + const { authorization } = req.headers; - if (!authorization) { - return res.status(403).send({ - type: "error", - payload: { - message: "The requeset did not include the Authorization header", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } + return res.status(403).send({ + type: "error", + payload: { + message: "The requeset did not include the Authorization header", + }, + }); + } - let token_payload_: string | JwtPayload; + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } - try { - token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); - } catch (e) { - if (e instanceof JsonWebTokenError) { + let token_payload_: string | JwtPayload; + + try { + token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET); + } catch (e) { + if (e instanceof JsonWebTokenError) { + return res.status(403).json({ + type: "error", + payload: { + message: "Token could not be verified; It might be expired", + }, + }); + } + + throw e; + } + + const token_payload = token_payload_ as AuthJWTPayload; + + if (!token_payload.permission_level || !token_payload.pid || !token_payload.revision) { + if (typeof (token_payload as unknown as TeamleaderJWTPayload).team === "string") { + return false; + } + + throw new AuthError("The token did not include the required information!"); + } + + const { pid, revision } = token_payload; + + let db_revision = await authClient.get(pid); + + if (db_revision === null) { + // Load the revision ID from the main DB and cache it in redis + const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } }); + + if (user) { + db_revision = user.revision.toISOString(); + + await authClient.set(pid, db_revision); + } + } + + if (revision !== db_revision || !revision || !db_revision) { return res.status(403).json({ type: "error", payload: { @@ -53,46 +99,79 @@ export const requireAuthentication = async (req: Request, res: Response, next: N }); } - throw e; - } + req.auth = { + isAuthenticated: true, + pid: token_payload.pid, + name: token_payload.name, + permission_level: token_payload.permission_level, + groups: token_payload.groups, + revision: token_payload.revision, + }; - const token_payload = token_payload_ as AuthJWTPayload; - - const { pid, revision } = token_payload; - - let db_revision = await authClient.get(pid); - - if (db_revision === null) { - // Load the revision ID from the main DB and cache it in redis - const user = await prisma.admin.findUnique({ where: { pid }, select: { revision: true } }); - - if (user) { - db_revision = user.revision.toISOString(); - - await authClient.set(pid, db_revision); + if (!config.controlled) { + next(); } - } - if (revision !== db_revision || !revision || !db_revision) { - return res.status(403).json({ - type: "error", - payload: { - message: "Token could not be verified; It might be expired", - }, - }); - } - - req.auth = { - isAuthenticated: true, - pid: token_payload.pid, - name: token_payload.name, - permission_level: token_payload.permission_level, - groups: token_payload.groups, - revision: token_payload.revision, + return true; }; - next(); -}; +export const requireAuthentication = _requireAdminAuthentication({ optional: false, controlled: false }); + +type AuthType = "admin" | "teamleader"; +interface AuthTypeConfig { + admin?: Boolean; + teamleader?: Boolean; +} + +interface AuthConfiguration { + type: AuthType | AuthTypeConfig; + + optional: Boolean; +} + +function getAuthTypes(type: AuthType | AuthTypeConfig): AuthType[] { + if (typeof type === "string") { + return [type]; + } + + return Object.entries(type) + .filter(([_, value]) => value) + .map(([key, _]) => key as AuthType); +} + +export const requireConfiguredAuthentication = + (config: AuthConfiguration = { optional: false, type: "admin" }) => + async (req: Request, res: Response, next: NextFunction) => { + const types = getAuthTypes(config.type); + const optional = config.optional; + + let adminFinished = false; + let teamleaderFinished = false; + + if (types.includes("admin")) { + adminFinished = Boolean(await _requireAdminAuthentication({ optional: true, controlled: true })(req, res, next)); + + if (adminFinished) { + return next(); + } + } + + if (types.includes("teamleader")) { + teamleaderFinished = Boolean( + _requireTeamleaderAuthentication({ optional: true, controlled: true })(req, res, next) + ); + + if (teamleaderFinished) { + return next(); + } + } + + if (!config.optional) { + throw new AuthError("No sufficient authorization was provided for this operation"); + } + + next(); + }; export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) { if (auth?.permission_level === "ELEVATED") { diff --git a/src/Middleware/auth/teamleaderAuth.ts b/src/Middleware/auth/teamleaderAuth.ts index aece12c..61fc82e 100644 --- a/src/Middleware/auth/teamleaderAuth.ts +++ b/src/Middleware/auth/teamleaderAuth.ts @@ -23,55 +23,67 @@ export function generateTeamleaderJWT(teamleader: Team) { return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" }); } -export async function requireTeamleaderAuthentication(req: Request, res: Response, next: NextFunction) { - if (!JWT_SECRET) { - throw new Error("JWT_SECRET not set"); - } +export const _requireTeamleaderAuthentication = + (config: { optional: Boolean; controlled: Boolean } = { optional: false, controlled: false }) => + (req: Request, res: Response, next: NextFunction) => { + if (!JWT_SECRET) { + throw new Error("JWT_SECRET not set"); + } - const { authorization } = req.headers; + const { authorization } = req.headers; - if (!authorization) { - return res.status(403).send({ - type: "error", - payload: { - message: - "The request did not include the Authorization header (Only the team leader can perform this operation)", - }, - }); - } + if (!authorization) { + if (config.optional) { + return false; + } - if (!verifyAuthorizationFormat(authorization)) { - return res.status(400).send({ - type: "error", - payload: { - message: "Malformed Authorization header", - format: "Bearer ", - }, - }); - } - - try { - const token_payload = jwt.verify(getBearerToken(authorization), JWT_SECRET) as TeamleaderJWTPayload; - - req.teamleader = { - isAuthenticated: true, - team: token_payload.team, - }; - - next(); - } catch (e) { - if (e instanceof JsonWebTokenError) { - return res.status(403).json({ + return res.status(403).send({ type: "error", payload: { - message: "Token could not be verified; It might be expired", + message: + "The request did not include the Authorization header (Only the team leader can perform this operation)", }, }); } - } - throw e; -} + if (!verifyAuthorizationFormat(authorization)) { + return res.status(400).send({ + type: "error", + payload: { + message: "Malformed Authorization header", + format: "Bearer ", + }, + }); + } + + try { + const token_payload = jwt.verify(getBearerToken(authorization), JWT_SECRET) as TeamleaderJWTPayload; + + req.teamleader = { + isAuthenticated: true, + team: token_payload.team, + }; + + if (!config.controlled) { + next(); + } + + return true; + } catch (e) { + if (e instanceof JsonWebTokenError) { + return res.status(403).json({ + type: "error", + payload: { + message: "Token could not be verified; It might be expired", + }, + }); + } + + throw e; + } + }; + +export const requireTeamleaderAuthentication = _requireTeamleaderAuthentication({ optional: false, controlled: false }); export function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) { if (auth?.team !== teamPid) {