From ef681be76c27cf44fd2eb9b6d57b36e7d11ac722 Mon Sep 17 00:00:00 2001 From: Pauline Vos Date: Wed, 13 Nov 2024 14:36:25 +0100 Subject: [PATCH] Create JWT auth middleware In order to verify a JWT and authorize access based on user roles. This assumes user roles will be stateless and included in the `aud` claim of the JWT --- src/auth/jwt.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++++ src/auth/role.ts | 15 +++++++ 2 files changed, 122 insertions(+) create mode 100644 src/auth/jwt.ts create mode 100644 src/auth/role.ts diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts new file mode 100644 index 0000000..a84b233 --- /dev/null +++ b/src/auth/jwt.ts @@ -0,0 +1,107 @@ +import { + ErrorType, + BadgeHubApiError, + NotAuthorizedError, + NotAuthenticatedError, +} from "../error"; +import { isAdmin, isContributor, UserRole } from "./role"; +import { jwtVerify, JWTPayload } from "jose"; +import { NextFunction, Request, Response } from "express"; + +// Middleware for routes that require a contributor role +const ensureAdminRouteMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + handleMiddlewareCheck(req, res, isAdmin); + + next(); +}; + +// Middleware for routes that require a contributor role +const ensureContributorRouteMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + handleMiddlewareCheck(req, res, isContributor); + + next(); +}; + +const handleMiddlewareCheck = ( + req: Request, + res: Response, + assertion: Function +) => { + try { + const token = decodeJwtFromRequest(req); + const role: UserRole[] = rolesFromJwtPayload(token); + assertion(role); + } catch (e) { + handleError(e, res); + } +}; + +const handleError = (err: Error, res) => { + if (err.name == ErrorType.NotAuthenticated) { + res.status(403).json({ error: err.message }); + } + if (err.name == ErrorType.NotAuthenticated) { + res.status(401).json({ error: err.message }); + } + + res.status(500).json({ error: "Internal server error" }); +}; + +const ensureAdmin = (role: UserRole) => { + if (!isAdmin(role)) { + throw NotAuthorizedError(); + } +}; + +const ensureContributor = (role: UserRole) => { + if (!isContributor(role)) { + throw NotAuthorizedError(); + } +}; + +const rolesFromJwtPayload = (payload: JWTPayload): UserRole[] => { + const aud = payload.aud; + + if (!aud) { + return []; + } + + if (aud instanceof String) { + return UserRole[aud as UserRole]; + } + + return (aud as string[]).map((i: string) => { + return UserRole[i as UserRole]; + }); +}; + +const decodeJwtFromRequest = (req: Request): JWTPayload => { + const token = req.headers.authorization; + + if (!token) { + throw NotAuthenticatedError("missing API token"); + } + + let payload: JWTPayload | string; + try { + payload = jwtVerify(token, process.env.JWT_SIGNING_KEY); + } catch (err) { + throw NotAuthenticatedError("API token invalid"); + } + + if (!Object.hasOwn(payload, "aud")) { + throw NotAuthenticatedError("API token invalid"); + } + + return payload; +}; + +export { ensureAdminRouteMiddleware, ensureContributorRouteMiddleware }; diff --git a/src/auth/role.ts b/src/auth/role.ts new file mode 100644 index 0000000..971003d --- /dev/null +++ b/src/auth/role.ts @@ -0,0 +1,15 @@ +enum UserRole { + Admin = "admin", + Contributor = "contributor", + ReadOnlyUser = "readonly", +} + +const isAdmin = (role: UserRole): boolean => { + return role == UserRole.Admin; +}; + +const isContributor = (role: UserRole): boolean => { + return role == UserRole.Contributor || isAdmin(role); +}; + +export { UserRole, isAdmin, isContributor };