Skip to content

Commit

Permalink
Merge pull request #29 from badgeteam/pauline/jwt
Browse files Browse the repository at this point in the history
Create auth middleware
  • Loading branch information
paulinevos authored Nov 15, 2024
2 parents 5d9cc0d + 5c8bf49 commit 1fb6e37
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 0 deletions.
117 changes: 117 additions & 0 deletions src/auth/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
ErrorType,
BadgeHubApiError,
NotAuthorizedError,
NotAuthenticatedError,
} from "../error";
import { isAdmin, isContributor, UserRole } from "./role";
import { JWTPayload, jwtVerify } from "jose";
import { NextFunction, Request, Response } from "express";

// Middleware for routes that require a contributor role
const ensureAdminRouteMiddleware = async (
req: Request,
res: Response,
next: NextFunction
) => {
await handleMiddlewareCheck(req, res, ensureAdmin);

next();
};

// Middleware for routes that require a contributor role
const ensureContributorRouteMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
handleMiddlewareCheck(req, res, ensureContributor).then(next);
};

const handleMiddlewareCheck = async (
req: Request,
res: Response,
assertion: Function
) => {
try {
const token = await decodeJwtFromRequest(req);
const roles: UserRole[] = rolesFromJwtPayload(token);

console.debug("Roles:", roles);
if (roles.length == 0) {
throw NotAuthorizedError();
}
assertion(roles);
} catch (e: any) {
handleError(e, res);
}
};

const handleError = (err: Error, res: Response) => {
if (err.name == ErrorType.NotAuthenticated) {
res.status(403).json({ error: err.message });
}
if (err.name == ErrorType.NotAuthorized) {
res.status(401).json({ error: err.message });
}

console.debug(err);
res.status(500).json({ error: "Internal server error" });
};

const ensureAdmin = (roles: UserRole[]) => {
roles.forEach((role) => {
if (!isAdmin(role)) {
throw NotAuthorizedError();
}
});
};

const ensureContributor = (roles: UserRole[]) => {
roles.forEach((role) => {
if (!isContributor(role)) {
throw NotAuthorizedError();
}
});
};

const rolesFromJwtPayload = (payload: JWTPayload): UserRole[] => {
const aud = payload.aud;

if (!aud) {
return [];
}

if (typeof aud == "string") {
return [UserRole[aud as UserRole]];
}

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

const decodeJwtFromRequest = async (req: Request): Promise<JWTPayload> => {
const token = req.headers.authorization;

if (!token) {
throw NotAuthenticatedError("missing API token");
}

try {
const result = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SIGNING_KEY)
);
const payload = result.payload;
if (!Object.hasOwn(payload, "aud")) {
throw NotAuthenticatedError("API token invalid");
}

return payload;
} catch (err) {
throw NotAuthenticatedError("API token invalid");
}
};

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 = "ReadOnlyUser",
}

const isAdmin = (role: UserRole): boolean => {
return role == UserRole.Admin;
};

const isContributor = (role: UserRole): boolean => {
return role == UserRole.Contributor || isAdmin(role);
};

export { UserRole, isAdmin, isContributor };
42 changes: 42 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
enum ErrorType {
NotAuthorized = "ERROR_NOT_AUTHORIZED",
NotAuthenticated = "ERROR_NOT_AUTHENTICATED",
}
class BadgeHubApiError extends Error {
override name: ErrorType;
override message: string;
override cause: any;

constructor({
name,
message,
cause,
}: {
name: ErrorType;
message: string;
cause?: any;
}) {
super();
this.name = name;
this.message = message;
this.cause = cause;
}
}
const NotAuthenticatedError = (
message: string = "You are not authenticated"
): BadgeHubApiError => {
return new BadgeHubApiError({ name: ErrorType.NotAuthenticated, message });
};

const NotAuthorizedError = (
message: string = "You are not authorized"
): BadgeHubApiError => {
return new BadgeHubApiError({ name: ErrorType.NotAuthorized, message });
};

export {
ErrorType,
BadgeHubApiError,
NotAuthorizedError,
NotAuthenticatedError,
};

0 comments on commit 1fb6e37

Please sign in to comment.