diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts new file mode 100644 index 0000000..37f9a9c --- /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 String]; + } + + return aud.map((i: string) => { + return UserRole[i]; + }); +}; + +const decodeJwtFromRequest = (req): 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 (token instanceof String || !Object.hasOwn(token, "role")) { + 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 };