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:
		
							parent
							
								
									054e761baa
								
							
						
					
					
						commit
						69ba8c9567
					
				
					 2 changed files with 29 additions and 16 deletions
				
			
		| 
						 | 
				
			
			@ -29,7 +29,7 @@ const idpConfigs = {
 | 
			
		|||
/**
 | 
			
		||||
 * 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) => {
 | 
			
		||||
        if (!token?.payload || !(token.payload as JwtPayload).iss) {
 | 
			
		||||
            throw new Error("Invalid token");
 | 
			
		||||
| 
						 | 
				
			
			@ -50,18 +50,19 @@ export const authenticateUser = expressjwt({
 | 
			
		|||
    },
 | 
			
		||||
    audience: getEnvVar(EnvVars.IdpAudience),
 | 
			
		||||
    algorithms: ["RS256"],
 | 
			
		||||
    credentialsRequired: false
 | 
			
		||||
    credentialsRequired: false,
 | 
			
		||||
    requestProperty: "jwtPayload"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get an object with information about the authenticated user from a given authenticated request.
 | 
			
		||||
 */
 | 
			
		||||
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
 | 
			
		||||
    if (!req.auth) {
 | 
			
		||||
    console.log("hi");
 | 
			
		||||
    if (!req.jwtPayload) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let issuer = req.auth.issuer;
 | 
			
		||||
    let issuer = req.jwtPayload.iss;
 | 
			
		||||
    let accountType: "student" | "teacher";
 | 
			
		||||
 | 
			
		||||
    if (issuer === idpConfigs.student.issuer) {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,29 +72,38 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
 | 
			
		|||
    } 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"],
 | 
			
		||||
        username: req.jwtPayload["preferred_username"]!,
 | 
			
		||||
        name: req.jwtPayload["name"],
 | 
			
		||||
        firstName: req.jwtPayload["given_name"],
 | 
			
		||||
        lastName: req.jwtPayload["family_name"],
 | 
			
		||||
        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
 | 
			
		||||
 * 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) => {
 | 
			
		||||
export const authorize = (accessCondition: (auth: AuthenticationInfo) => boolean) => {
 | 
			
		||||
    return (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
 | 
			
		||||
        let authInfo = getAuthenticationInfo(req);
 | 
			
		||||
        if (!authInfo) {
 | 
			
		||||
        if (!req.auth) {
 | 
			
		||||
            res.status(401).json({ message: "Unauthorized" });
 | 
			
		||||
        } else if (!accessCondition(authInfo)) {
 | 
			
		||||
        } else if (!accessCondition(req.auth)) {
 | 
			
		||||
            res.status(403).json({ message: "Forbidden" });
 | 
			
		||||
        } else {
 | 
			
		||||
            next();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,9 @@
 | 
			
		|||
import { Request } from "express";
 | 
			
		||||
import { JwtPayload } from "jsonwebtoken";
 | 
			
		||||
import {AuthenticationInfo} from "./authentication-info";
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue