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
							
								
									be667c7c53
								
							
						
					
					
						commit
						054e761baa
					
				
					 2 changed files with 71 additions and 18 deletions
				
			
		|  | @ -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"); | ||||
|  |  | |||
							
								
								
									
										11
									
								
								backend/src/middleware/auth/authentication-info.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/middleware/auth/authentication-info.ts
									
										
									
									
									
										Normal 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 | ||||
| }; | ||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger