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:
Gerald Schmittinger 2025-03-01 23:09:42 +01:00
parent 228615af98
commit be667c7c53
9 changed files with 722 additions and 2789 deletions

View file

@ -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

View file

@ -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"

View file

@ -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);

View 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);

View 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
}

View file

@ -41,14 +41,14 @@ router.get('/:id/submissions', (req, res) => {
});
})
// the list of assignments a student has
router.get('/:id/assignments', (req, res) => {
res.json({
assignments: [ '0' ],
});
})
// the list of groups a student is in
router.get('/:id/groups', (req, res) => {
res.json({
@ -56,4 +56,4 @@ router.get('/:id/groups', (req, res) => {
});
})
export default router
export default router

View file

@ -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;
/**

View file

@ -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

File diff suppressed because it is too large Load diff