feat(backend): Auth middleware toegevoegd.
Deze verifiëert het meegegeven bearer token. Door een specifieke extra middleware per endpoint kan dan aangegeven worden of deze enkel toegankelijk is voor leerlingen, leerkrachten of allebei.
This commit is contained in:
		
							parent
							
								
									228615af98
								
							
						
					
					
						commit
						be667c7c53
					
				
					 9 changed files with 722 additions and 2789 deletions
				
			
		|  | @ -4,3 +4,8 @@ DWENGO_DB_PORT=5431 | ||||||
| DWENGO_DB_USERNAME=postgres | DWENGO_DB_USERNAME=postgres | ||||||
| DWENGO_DB_PASSWORD=postgres | DWENGO_DB_PASSWORD=postgres | ||||||
| DWENGO_DB_UPDATE=true | DWENGO_DB_UPDATE=true | ||||||
|  | 
 | ||||||
|  | DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||||
|  | DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | ||||||
|  | DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | ||||||
|  | DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ | ||||||
|         "@mikro-orm/reflection": "6.4.6", |         "@mikro-orm/reflection": "6.4.6", | ||||||
|         "dotenv": "^16.4.7", |         "dotenv": "^16.4.7", | ||||||
|         "express": "^5.0.1", |         "express": "^5.0.1", | ||||||
|  |         "express-jwt": "^8.5.1", | ||||||
|  |         "jwks-rsa": "^3.1.0", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|         "js-yaml": "^4.1.0", |         "js-yaml": "^4.1.0", | ||||||
|         "@types/js-yaml": "^4.0.9" |         "@types/js-yaml": "^4.0.9" | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import submissionRouter from './routes/submission.js'; | ||||||
| import classRouter from './routes/class.js'; | import classRouter from './routes/class.js'; | ||||||
| import questionRouter from './routes/question.js'; | import questionRouter from './routes/question.js'; | ||||||
| import loginRouter from './routes/login.js'; | import loginRouter from './routes/login.js'; | ||||||
|  | import {authenticateUser} from "./middleware/auth/auth"; | ||||||
| 
 | 
 | ||||||
| const app: Express = express(); | const app: Express = express(); | ||||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||||
|  | @ -23,6 +24,8 @@ app.get('/', (_, res: Response) => { | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | app.use(authenticateUser); | ||||||
|  | 
 | ||||||
| app.use('/student', studentRouter); | app.use('/student', studentRouter); | ||||||
| app.use('/group', groupRouter); | app.use('/group', groupRouter); | ||||||
| app.use('/assignment', assignmentRouter); | app.use('/assignment', assignmentRouter); | ||||||
|  |  | ||||||
							
								
								
									
										75
									
								
								backend/src/middleware/auth/auth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								backend/src/middleware/auth/auth.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | import {EnvVars, getEnvVar} from "../../util/envvars.js"; | ||||||
|  | import {expressjwt} from 'express-jwt'; | ||||||
|  | import {JwtPayload} from 'jsonwebtoken' | ||||||
|  | import jwksClient from 'jwks-rsa'; | ||||||
|  | import * as express from "express"; | ||||||
|  | import * as jwt from "jsonwebtoken"; | ||||||
|  | import {AuthenticatedRequest} from "./authenticated-request.js"; | ||||||
|  | 
 | ||||||
|  | function createJwksClient(uri: string): jwksClient.JwksClient { | ||||||
|  |     return jwksClient({ | ||||||
|  |         cache: true, | ||||||
|  |         rateLimit: true, | ||||||
|  |         jwksUri: uri, | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const idpConfigs = { | ||||||
|  |     student: { | ||||||
|  |         issuer: getEnvVar(EnvVars.IdpStudentUrl), | ||||||
|  |         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), | ||||||
|  |     }, | ||||||
|  |     teacher: { | ||||||
|  |         issuer: getEnvVar(EnvVars.IdpTeacherUrl), | ||||||
|  |         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const authenticateUser = expressjwt({ | ||||||
|  |     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { | ||||||
|  |         if (!token?.payload || !(token.payload as JwtPayload).iss) { | ||||||
|  |             throw new Error("Invalid token"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let issuer = (token.payload as JwtPayload).iss; | ||||||
|  | 
 | ||||||
|  |         let idpConfig = Object.values(idpConfigs).find(config => config.issuer === issuer); | ||||||
|  |         if (!idpConfig) { | ||||||
|  |             throw new Error("Issuer not accepted."); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); | ||||||
|  |         if (!signingKey) { | ||||||
|  |             throw new Error("Signing key not found."); | ||||||
|  |         } | ||||||
|  |         return signingKey.getPublicKey(); | ||||||
|  |     }, | ||||||
|  |     audience: getEnvVar(EnvVars.IdpAudience), | ||||||
|  |     algorithms: ["RS256"], | ||||||
|  |     credentialsRequired: false | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const authorizeRole = (studentsAllowed: boolean, teachersAllowed: boolean) => { | ||||||
|  |     return (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => { | ||||||
|  |         if (!req.auth) { | ||||||
|  |             res.status(401).json({ message: "Unauthorized" }); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         next(); // User is allowed
 | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const authenticatedOnly = authorizeRole(true, true); | ||||||
|  | export const studentsOnly = authorizeRole(true, false); | ||||||
|  | export const teachersOnly = authorizeRole(false, true); | ||||||
							
								
								
									
										6
									
								
								backend/src/middleware/auth/authenticated-request.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/src/middleware/auth/authenticated-request.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | import { Request } from "express"; | ||||||
|  | import { JwtPayload } from "jsonwebtoken"; | ||||||
|  | 
 | ||||||
|  | export interface AuthenticatedRequest extends Request { | ||||||
|  |     auth?: JwtPayload; // Optional, as req.auth might be undefined if authentication is optional
 | ||||||
|  | } | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| const PREFIX = 'DWENGO_'; | const PREFIX = 'DWENGO_'; | ||||||
| const DB_PREFIX = PREFIX + 'DB_'; | const DB_PREFIX = PREFIX + 'DB_'; | ||||||
|  | const IDP_PREFIX = PREFIX + 'AUTH_'; | ||||||
|  | const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||||
|  | const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||||
| 
 | 
 | ||||||
| type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||||
| 
 | 
 | ||||||
|  | @ -11,6 +14,11 @@ export const EnvVars: { [key: string]: EnvVar } = { | ||||||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, |     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, |     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||||
|     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, |     DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, | ||||||
|  |     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||||
|  |     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||||
|  |     IdpTeacherUrl: { key: TEACHER_IDP_PREFIX + 'URL', required: true }, | ||||||
|  |     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||||
|  |     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' } | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -9,6 +9,24 @@ services: | ||||||
|       - "5431:5432" |       - "5431:5432" | ||||||
|     volumes: |     volumes: | ||||||
|         - postgres_data:/var/lib/postgresql/data |         - postgres_data:/var/lib/postgresql/data | ||||||
| 
 |   idp: # Bron: https://medium.com/@fingervinicius/easy-running-keycloak-with-docker-compose-b0d7a4ee2358 | ||||||
|  |       image: quay.io/keycloak/keycloak:latest | ||||||
|  |       environment: | ||||||
|  |           KC_HOSTNAME: localhost | ||||||
|  |           KC_HOSTNAME_PORT: 7080 | ||||||
|  |           KC_HOSTNAME_STRICT_BACKCHANNEL: "true" | ||||||
|  |           KEYCLOAK_ADMIN: admin | ||||||
|  |           KEYCLOAK_ADMIN_PASSWORD: admin | ||||||
|  |           KC_HEALTH_ENABLED: "true" | ||||||
|  |           KC_LOG_LEVEL: info | ||||||
|  |       healthcheck: | ||||||
|  |           test: [ "CMD", "curl", "-f", "http://localhost:7080/health/ready" ] | ||||||
|  |           interval: 15s | ||||||
|  |           timeout: 2s | ||||||
|  |           retries: 15 | ||||||
|  |       command: ["start-dev", "--http-port", "7080", "--https-port", "7443"] | ||||||
|  |       ports: | ||||||
|  |        - "7080:7080" | ||||||
|  |        - "7443:7443" | ||||||
| volumes: | volumes: | ||||||
|     postgres_data: |     postgres_data: | ||||||
|  |  | ||||||
							
								
								
									
										3386
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3386
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger