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-01 23:47:32 +01:00
parent be667c7c53
commit 054e761baa
2 changed files with 71 additions and 18 deletions

View file

@ -5,6 +5,7 @@ import jwksClient from 'jwks-rsa';
import * as express from "express";
import * as jwt from "jsonwebtoken";
import {AuthenticatedRequest} from "./authenticated-request.js";
import {AuthenticationInfo} from "./authentication-info";
function createJwksClient(uri: string): jwksClient.JwksClient {
return jwksClient({
@ -25,6 +26,9 @@ const idpConfigs = {
}
};
/**
* Express middleware which verifies the JWT Bearer token if one is given in the request.
*/
export const authenticateUser = expressjwt({
secret: async (_: express.Request, token: jwt.Jwt | undefined) => {
if (!token?.payload || !(token.payload as JwtPayload).iss) {
@ -49,27 +53,65 @@ export const authenticateUser = expressjwt({
credentialsRequired: false
});
const authorizeRole = (studentsAllowed: boolean, teachersAllowed: boolean) => {
/**
* Get an object with information about the authenticated user from a given authenticated request.
*/
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
if (!req.auth) {
return;
}
let issuer = req.auth.issuer;
let accountType: "student" | "teacher";
if (issuer === idpConfigs.student.issuer) {
accountType = "student";
} else if (issuer === idpConfigs.teacher.issuer) {
accountType = "teacher";
} else {
return;
}
return {
accountType: accountType,
username: req.auth["preferred_username"]!,
name: req.auth["name"],
firstName: req.auth["given_name"],
lastName: req.auth["family_name"],
email: req.auth["email"],
}
}
/**
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
* the given access condition.
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true.
*/
const authorize = (accessCondition: (auth: AuthenticationInfo) => boolean) => {
return (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
if (!req.auth) {
let authInfo = getAuthenticationInfo(req);
if (!authInfo) {
res.status(401).json({ message: "Unauthorized" });
return;
} else if (!accessCondition(authInfo)) {
res.status(403).json({ message: "Forbidden" });
} else {
next();
}
}
}
const issuer = req.auth.iss;
if (issuer === idpConfigs.student.issuer && !studentsAllowed) {
res.status(403).json({ message: "Students not allowed" });
return;
}
if (issuer === idpConfigs.teacher.issuer && !teachersAllowed) {
res.status(403).json({ message: "Teachers not allowed" });
return;
}
/**
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
*/
export const authenticatedOnly = authorize(_ => true);
next(); // User is allowed
};
};
/**
* Middleware which rejects requests from unauthenticated users or users that aren't students.
*/
export const studentsOnly = authorize(auth => auth.accountType === "student");
export const authenticatedOnly = authorizeRole(true, true);
export const studentsOnly = authorizeRole(true, false);
export const teachersOnly = authorizeRole(false, true);
/**
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
*/
export const teachersOnly = authorize(auth => auth.accountType === "teacher");

View file

@ -0,0 +1,11 @@
/**
* Object with information about the user who is currently logged in.
*/
export type AuthenticationInfo = {
accountType: "student" | "teacher",
username: string,
name?: string,
firstName?: string,
lastName?: string,
email?: string
};