feat(backend): Generic authentication checks.

Added support for deciding based on any predicate about the current AuthenticationInfo whether or not a request will be accepted.
This commit is contained in:
Gerald Schmittinger 2025-03-02 01:24:49 +01:00
parent 054e761baa
commit 69ba8c9567
2 changed files with 29 additions and 16 deletions

View file

@ -29,7 +29,7 @@ const idpConfigs = {
/** /**
* Express middleware which verifies the JWT Bearer token if one is given in the request. * Express middleware which verifies the JWT Bearer token if one is given in the request.
*/ */
export const authenticateUser = expressjwt({ const verifyJwtToken = expressjwt({
secret: async (_: express.Request, token: jwt.Jwt | undefined) => { secret: async (_: express.Request, token: jwt.Jwt | undefined) => {
if (!token?.payload || !(token.payload as JwtPayload).iss) { if (!token?.payload || !(token.payload as JwtPayload).iss) {
throw new Error("Invalid token"); throw new Error("Invalid token");
@ -50,18 +50,19 @@ export const authenticateUser = expressjwt({
}, },
audience: getEnvVar(EnvVars.IdpAudience), audience: getEnvVar(EnvVars.IdpAudience),
algorithms: ["RS256"], algorithms: ["RS256"],
credentialsRequired: false credentialsRequired: false,
requestProperty: "jwtPayload"
}); });
/** /**
* Get an object with information about the authenticated user from a given authenticated request. * Get an object with information about the authenticated user from a given authenticated request.
*/ */
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.auth) { console.log("hi");
if (!req.jwtPayload) {
return; return;
} }
let issuer = req.jwtPayload.iss;
let issuer = req.auth.issuer;
let accountType: "student" | "teacher"; let accountType: "student" | "teacher";
if (issuer === idpConfigs.student.issuer) { if (issuer === idpConfigs.student.issuer) {
@ -71,29 +72,38 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
} else { } else {
return; return;
} }
return { return {
accountType: accountType, accountType: accountType,
username: req.auth["preferred_username"]!, username: req.jwtPayload["preferred_username"]!,
name: req.auth["name"], name: req.jwtPayload["name"],
firstName: req.auth["given_name"], firstName: req.jwtPayload["given_name"],
lastName: req.auth["family_name"], lastName: req.jwtPayload["family_name"],
email: req.auth["email"], email: req.jwtPayload["email"],
} }
} }
/**
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
* to avoid that the routers have to deal with the JWT token.
*/
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
req.auth = getAuthenticationInfo(req);
next();
};
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
/** /**
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
* the given access condition. * the given access condition.
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true. * to true.
*/ */
const authorize = (accessCondition: (auth: AuthenticationInfo) => boolean) => { export const authorize = (accessCondition: (auth: AuthenticationInfo) => boolean) => {
return (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => { return (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
let authInfo = getAuthenticationInfo(req); if (!req.auth) {
if (!authInfo) {
res.status(401).json({ message: "Unauthorized" }); res.status(401).json({ message: "Unauthorized" });
} else if (!accessCondition(authInfo)) { } else if (!accessCondition(req.auth)) {
res.status(403).json({ message: "Forbidden" }); res.status(403).json({ message: "Forbidden" });
} else { } else {
next(); next();

View file

@ -1,6 +1,9 @@
import { Request } from "express"; import { Request } from "express";
import { JwtPayload } from "jsonwebtoken"; import { JwtPayload } from "jsonwebtoken";
import {AuthenticationInfo} from "./authentication-info";
export interface AuthenticatedRequest extends Request { export interface AuthenticatedRequest extends Request {
auth?: JwtPayload; // Optional, as req.auth might be undefined if authentication is optional // Properties are optional since the user is not necessarily authenticated.
jwtPayload?: JwtPayload;
auth?: AuthenticationInfo;
} }