Skip to content

Commit

Permalink
Create JWT auth middleware
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Pauline Vos committed Nov 15, 2024
1 parent 6620b04 commit ef681be
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 0 deletions.
107 changes: 107 additions & 0 deletions src/auth/jwt.ts
Original file line number Diff line number Diff line change
@@ -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);

Check failure on line 43 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'unknown' is not assignable to parameter of type 'Error'.
}
};

const handleError = (err: Error, res) => {

Check failure on line 47 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Parameter 'res' implicitly has an 'any' type.
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];

Check failure on line 78 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Property 'admin' does not exist on type 'typeof UserRole'. Did you mean 'Admin'?
}

return (aud as string[]).map((i: string) => {
return UserRole[i as UserRole];

Check failure on line 82 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Property 'admin' does not exist on type 'typeof UserRole'. Did you mean 'Admin'?
});
};

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);

Check failure on line 95 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Type 'Promise<JWTVerifyResult<JWTPayload>>' is not assignable to type 'string | JWTPayload'.

Check failure on line 95 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

No overload matches this call.
} catch (err) {
throw NotAuthenticatedError("API token invalid");
}

if (!Object.hasOwn(payload, "aud")) {

Check failure on line 100 in src/auth/jwt.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'string | JWTPayload' is not assignable to parameter of type 'object'.
throw NotAuthenticatedError("API token invalid");
}

return <JWTPayload>payload;
};

export { ensureAdminRouteMiddleware, ensureContributorRouteMiddleware };
15 changes: 15 additions & 0 deletions src/auth/role.ts
Original file line number Diff line number Diff line change
@@ -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 };

0 comments on commit ef681be

Please sign in to comment.