diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index a91932ea..54354a55 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,13 +1,11 @@ -import { envVars, getEnvVar } from '../../util/envVars.js'; -import { expressjwt } from 'express-jwt'; +import {envVars, getEnvVar} from '../../util/envVars.js'; +import {expressjwt} from 'express-jwt'; import * as jwt from 'jsonwebtoken'; -import { JwtPayload } from 'jsonwebtoken'; +import {JwtPayload} from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as express from 'express'; -import { AuthenticatedRequest } from './authenticated-request.js'; -import { AuthenticationInfo } from './authentication-info.js'; -import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; -import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; +import {AuthenticatedRequest} from './authenticated-request.js'; +import {AuthenticationInfo} from './authentication-info.js'; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; @@ -108,36 +106,3 @@ function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response } 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. - */ -export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { - return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { - if (!req.auth) { - throw new UnauthorizedException(); - } else if (!accessCondition(req.auth)) { - throw new ForbiddenException(); - } else { - next(); - } - }; -} - -/** - * Middleware which rejects all unauthenticated users, but accepts all authenticated users. - */ -export const authenticatedOnly = authorize((_) => true); - -/** - * Middleware which rejects requests from unauthenticated users or users that aren't students. - */ -export const studentsOnly = authorize((auth) => auth.accountType === 'student'); - -/** - * Middleware which rejects requests from unauthenticated users or users that aren't teachers. - */ -export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts new file mode 100644 index 00000000..bf15e551 --- /dev/null +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -0,0 +1,38 @@ +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import * as express from "express"; +import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; +import {ForbiddenException} from "../../../exceptions/forbidden-exception"; + +/** + * 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. + */ +export function authorize( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +) { + return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise => { + if (!req.auth) { + throw new UnauthorizedException(); + } else if (!await accessCondition(req.auth, req)) { + throw new ForbiddenException(); + } else { + next(); + } + }; +} + +/** + * Middleware which rejects all unauthenticated users, but accepts all authenticated users. + */ +export const authenticatedOnly = authorize((_) => true); +/** + * Middleware which rejects requests from unauthenticated users or users that aren't students. + */ +export const studentsOnly = authorize((auth) => auth.accountType === 'student'); +/** + * Middleware which rejects requests from unauthenticated users or users that aren't teachers. + */ +export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts new file mode 100644 index 00000000..6c92e190 --- /dev/null +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -0,0 +1,22 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import {getClassesByTeacher} from "../../../services/teachers"; + +/** + * To be used on a request with path parameters username and classId. + * Only allows requests whose username parameter is equal to the username of the user who is logged in and requests + * whose classId parameter references a class the logged-in user is a teacher of. + */ +export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + if (req.params.username === auth.username) { + return true; + } else if (auth.accountType === "teacher") { + const classes: string[] = (await getClassesByTeacher(auth.username, false)) as string[]; + return req.params.classId in classes; + } else { + return false; + } + } +); diff --git a/backend/src/middleware/auth/checks/user-auth-checks.ts b/backend/src/middleware/auth/checks/user-auth-checks.ts new file mode 100644 index 00000000..10814eb8 --- /dev/null +++ b/backend/src/middleware/auth/checks/user-auth-checks.ts @@ -0,0 +1,10 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; + +/** + * Only allow the user whose username is in the path parameter "username" to access the endpoint. + */ +export const onlyAllowUserHimself = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username +); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 4a1f27d2..8c4ab450 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,6 +1,7 @@ import express from 'express'; import { getFrontendAuthConfig } from '../controllers/auth.js'; -import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; +import {authenticatedOnly, studentsOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; + const router = express.Router(); // Returns auth configuration for frontend diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index daf79f09..66a6c75e 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -5,15 +5,19 @@ import { getStudentRequestHandler, getStudentRequestsHandler, } from '../controllers/students.js'; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {onlyAllowStudentHimselfAndTeachersOfClass} from "../middleware/auth/checks/class-auth-checks"; + +// Under /:username/joinRequests/ const router = express.Router({ mergeParams: true }); -router.get('/', getStudentRequestsHandler); +router.get('/', onlyAllowUserHimself, getStudentRequestsHandler); -router.post('/', createStudentRequestHandler); +router.post('/', onlyAllowUserHimself, createStudentRequestHandler); -router.get('/:classId', getStudentRequestHandler); +router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler); -router.delete('/:classId', deleteClassJoinRequestHandler); +router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler); export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 0f5d5349..54b0d894 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -11,6 +11,7 @@ import { getStudentSubmissionsHandler, } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; const router = express.Router(); @@ -19,25 +20,25 @@ router.get('/', getAllStudentsHandler); router.post('/', createStudentHandler); -router.delete('/:username', deleteStudentHandler); +router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); // Information about a student's profile -router.get('/:username', getStudentHandler); +router.get('/:username', onlyAllowUserHimself, getStudentHandler); // The list of classes a student is in -router.get('/:username/classes', getStudentClassesHandler); +router.get('/:username/classes', onlyAllowUserHimself, getStudentClassesHandler); // The list of submissions a student has made -router.get('/:username/submissions', getStudentSubmissionsHandler); +router.get('/:username/submissions', onlyAllowUserHimself, getStudentSubmissionsHandler); // The list of assignments a student has -router.get('/:username/assignments', getStudentAssignmentsHandler); +router.get('/:username/assignments', onlyAllowUserHimself, getStudentAssignmentsHandler); // The list of groups a student is in -router.get('/:username/groups', getStudentGroupsHandler); +router.get('/:username/groups', onlyAllowUserHimself, getStudentGroupsHandler); // A list of questions a user has created -router.get('/:username/questions', getStudentQuestionsHandler); +router.get('/:username/questions', onlyAllowUserHimself, getStudentQuestionsHandler); router.use('/:username/joinRequests', joinRequestRouter);