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_PASSWORD=postgres
|
||||
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",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.0.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
|
|
|
@ -11,6 +11,7 @@ import submissionRouter from './routes/submission.js';
|
|||
import classRouter from './routes/class.js';
|
||||
import questionRouter from './routes/question.js';
|
||||
import loginRouter from './routes/login.js';
|
||||
import {authenticateUser} from "./middleware/auth/auth";
|
||||
|
||||
const app: Express = express();
|
||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
||||
|
@ -23,6 +24,8 @@ app.get('/', (_, res: Response) => {
|
|||
});
|
||||
});
|
||||
|
||||
app.use(authenticateUser);
|
||||
|
||||
app.use('/student', studentRouter);
|
||||
app.use('/group', groupRouter);
|
||||
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 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 };
|
||||
|
||||
|
@ -11,6 +14,11 @@ export const EnvVars: { [key: string]: EnvVar } = {
|
|||
DbUsername: { key: DB_PREFIX + 'USERNAME', required: true },
|
||||
DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true },
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,24 @@ services:
|
|||
- "5431:5432"
|
||||
volumes:
|
||||
- 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:
|
||||
postgres_data:
|
||||
|
|
3386
package-lock.json
generated
3386
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue