mirror of
https://github.com/detleph/server.git
synced 2026-09-07 23:44:40 +02:00
Refractor auth logic
+ Add possibility (_requireAdminAuthentication, _requireTeamleaderAuthentication) to make auth optional + Add possibility to combine multiple auth types into one function
This commit is contained in:
@@ -52,6 +52,7 @@ if [ "$RECREATE" = true ]; then
|
|||||||
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
|
-e DATABASE_URL="postgresql://server:server@postgres:5432/management?schema=public" \
|
||||||
-e NODE_ENV="development" \
|
-e NODE_ENV="development" \
|
||||||
-e PORT="${D_PORT}" \
|
-e PORT="${D_PORT}" \
|
||||||
|
-e JWT_SECRET="not_for_production" \
|
||||||
-p "${D_PORT}":"${D_PORT}" \
|
-p "${D_PORT}":"${D_PORT}" \
|
||||||
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
|
--entrypoint "/app/scripts/docker-entrypoint.dev.sh" \
|
||||||
node
|
node
|
||||||
|
|||||||
+141
-62
@@ -6,6 +6,9 @@ import { authClient } from "../../lib/redis";
|
|||||||
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
|
import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken";
|
||||||
import prisma from "../../lib/prisma";
|
import prisma from "../../lib/prisma";
|
||||||
import AuthError from "../error/AuthError";
|
import AuthError from "../error/AuthError";
|
||||||
|
import { TeamleaderJWTPayload, _requireTeamleaderAuthentication } from "./teamleaderAuth";
|
||||||
|
|
||||||
|
require("express-async-errors");
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
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 getBearerToken = (authorization: string) => authorization.slice(7);
|
||||||
|
|
||||||
export const requireAuthentication = async (req: Request, res: Response, next: NextFunction) => {
|
const _requireAdminAuthentication =
|
||||||
if (!JWT_SECRET) {
|
(config: { optional?: Boolean; controlled?: Boolean } = { optional: false, controlled: false }) =>
|
||||||
throw new Error("JWT_SECRET not set");
|
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) {
|
if (!authorization) {
|
||||||
return res.status(403).send({
|
if (config.optional) {
|
||||||
type: "error",
|
return false;
|
||||||
payload: {
|
}
|
||||||
message: "The requeset did not include the Authorization header",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verifyAuthorizationFormat(authorization)) {
|
return res.status(403).send({
|
||||||
return res.status(400).send({
|
type: "error",
|
||||||
type: "error",
|
payload: {
|
||||||
payload: {
|
message: "The requeset did not include the Authorization header",
|
||||||
message: "Malformed Authorization header",
|
},
|
||||||
format: "Bearer <token>",
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let token_payload_: string | JwtPayload;
|
if (!verifyAuthorizationFormat(authorization)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
type: "error",
|
||||||
|
payload: {
|
||||||
|
message: "Malformed Authorization header",
|
||||||
|
format: "Bearer <token>",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
let token_payload_: string | JwtPayload;
|
||||||
token_payload_ = jwt.verify(getBearerToken(authorization), JWT_SECRET);
|
|
||||||
} catch (e) {
|
try {
|
||||||
if (e instanceof JsonWebTokenError) {
|
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({
|
return res.status(403).json({
|
||||||
type: "error",
|
type: "error",
|
||||||
payload: {
|
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;
|
if (!config.controlled) {
|
||||||
|
next();
|
||||||
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 true;
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) {
|
export function requireResponsibleForGroup(auth: AuthJWTPayload | undefined, groupPid: string) {
|
||||||
if (auth?.permission_level === "ELEVATED") {
|
if (auth?.permission_level === "ELEVATED") {
|
||||||
|
|||||||
@@ -23,55 +23,67 @@ export function generateTeamleaderJWT(teamleader: Team) {
|
|||||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" });
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: "4 days" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireTeamleaderAuthentication(req: Request, res: Response, next: NextFunction) {
|
export const _requireTeamleaderAuthentication =
|
||||||
if (!JWT_SECRET) {
|
(config: { optional: Boolean; controlled: Boolean } = { optional: false, controlled: false }) =>
|
||||||
throw new Error("JWT_SECRET not set");
|
(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) {
|
if (!authorization) {
|
||||||
return res.status(403).send({
|
if (config.optional) {
|
||||||
type: "error",
|
return false;
|
||||||
payload: {
|
}
|
||||||
message:
|
|
||||||
"The request did not include the Authorization header (Only the team leader can perform this operation)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verifyAuthorizationFormat(authorization)) {
|
return res.status(403).send({
|
||||||
return res.status(400).send({
|
|
||||||
type: "error",
|
|
||||||
payload: {
|
|
||||||
message: "Malformed Authorization header",
|
|
||||||
format: "Bearer <token>",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
|
||||||
type: "error",
|
type: "error",
|
||||||
payload: {
|
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 <token>",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
export function requireLeaderOfTeam(auth: TeamleaderJWTPayload | undefined, teamPid: string) {
|
||||||
if (auth?.team !== teamPid) {
|
if (auth?.team !== teamPid) {
|
||||||
|
|||||||
Reference in New Issue
Block a user