From 147a3348685285baeaeb2f4fd9fc8f5d577c8b6c Mon Sep 17 00:00:00 2001 From: juancarmore Date: Tue, 25 Mar 2025 13:10:08 +0100 Subject: [PATCH] backend: Refactor middlewares and routes to configure authentication --- backend/src/middlewares/auth.middleware.ts | 35 ++++++++++ backend/src/middlewares/index.ts | 2 + .../src/middlewares/participant.middleware.ts | 67 +++++++++++++++++++ .../src/middlewares/recording.middleware.ts | 42 +++++++++--- backend/src/middlewares/room.middleware.ts | 33 +++++++++ backend/src/routes/auth.routes.ts | 9 +-- backend/src/routes/participants.routes.ts | 12 +++- backend/src/routes/recording.routes.ts | 46 +++++++++---- backend/src/routes/room.routes.ts | 18 ++--- 9 files changed, 222 insertions(+), 42 deletions(-) create mode 100644 backend/src/middlewares/participant.middleware.ts create mode 100644 backend/src/middlewares/room.middleware.ts diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 2e38afb..db1575d 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -104,3 +104,38 @@ export const apiKeyValidator = async (req: Request) => { throw errorInvalidApiKey(); } }; + +// Allow anonymous access +export const allowAnonymous = async (req: Request) => { + const anonymousUser = { + username: 'anonymous', + role: UserRole.USER + }; + + req.session = req.session || {}; + req.session.user = anonymousUser; +}; + +export const configureProfileAuth = async (req: Request, res: Response, next: NextFunction) => { + const logger = container.get(LoggerService); + const globalPrefService = container.get(GlobalPreferencesService); + let requireAuthForRoomCreation: boolean; + let authMode: AuthMode; + + try { + const { securityPreferences } = await globalPrefService.getGlobalPreferences(); + requireAuthForRoomCreation = securityPreferences.roomCreationPolicy.requireAuthentication; + authMode = securityPreferences.authentication.authMode; + } catch (error) { + logger.error('Error checking authentication preferences:' + error); + return res.status(500).json({ message: 'Internal server error' }); + } + + const authValidators = [tokenAndRoleValidator(UserRole.ADMIN)]; + + if (requireAuthForRoomCreation || authMode !== AuthMode.NONE) { + authValidators.push(tokenAndRoleValidator(UserRole.USER)); + } + + return withAuth(...authValidators)(req, res, next); +}; diff --git a/backend/src/middlewares/index.ts b/backend/src/middlewares/index.ts index 7a6f77a..77705e6 100644 --- a/backend/src/middlewares/index.ts +++ b/backend/src/middlewares/index.ts @@ -1,4 +1,6 @@ export * from './auth.middleware.js'; +export * from './room.middleware.js'; +export * from './participant.middleware.js'; export * from './recording.middleware.js'; export * from './content-type.middleware.js'; export * from './request-validators/participant-validator.middleware.js'; diff --git a/backend/src/middlewares/participant.middleware.ts b/backend/src/middlewares/participant.middleware.ts new file mode 100644 index 0000000..ab326af --- /dev/null +++ b/backend/src/middlewares/participant.middleware.ts @@ -0,0 +1,67 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthMode, ParticipantRole, UserRole, TokenOptions } from '@typings-ce'; +import { container } from '../config/dependency-injector.config.js'; +import { GlobalPreferencesService, LoggerService, RoomService } from '../services/index.js'; +import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; + +export const configureTokenAuth = async (req: Request, res: Response, next: NextFunction) => { + const logger = container.get(LoggerService); + const globalPrefService = container.get(GlobalPreferencesService); + const roomService = container.get(RoomService); + + let role: ParticipantRole; + + try { + const { roomName, secret } = req.body as TokenOptions; + role = await roomService.getRoomSecretRole(roomName, secret); + } catch (error) { + logger.error('Error getting room secret role', error); + return res.status(500).json({ message: 'Internal server error' }); + } + + let authMode: AuthMode; + + try { + const { securityPreferences } = await globalPrefService.getGlobalPreferences(); + authMode = securityPreferences.authentication.authMode; + } catch (error) { + logger.error('Error checking authentication preferences', error); + return res.status(500).json({ message: 'Internal server error' }); + } + + const authValidators = []; + + if (authMode === AuthMode.NONE) { + authValidators.push(allowAnonymous); + } else { + const isModeratorsOnlyMode = authMode === AuthMode.MODERATORS_ONLY && role === ParticipantRole.MODERATOR; + const isAllUsersMode = authMode === AuthMode.ALL_USERS; + + if (isModeratorsOnlyMode || isAllUsersMode) { + authValidators.push(tokenAndRoleValidator(UserRole.USER)); + } else { + authValidators.push(allowAnonymous); + } + } + + return withAuth(...authValidators)(req, res, next); +}; + +export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { + const roomName = req.query.roomName as string; + const payload = req.session?.tokenClaims; + + if (!payload) { + return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + } + + const sameRoom = payload.video?.room === roomName; + const metadata = JSON.parse(payload.metadata || '{}'); + const role = metadata.role as ParticipantRole; + + if (!sameRoom || role !== ParticipantRole.MODERATOR) { + return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + } + + return next(); +}; diff --git a/backend/src/middlewares/recording.middleware.ts b/backend/src/middlewares/recording.middleware.ts index f3e5352..1cfbb9d 100644 --- a/backend/src/middlewares/recording.middleware.ts +++ b/backend/src/middlewares/recording.middleware.ts @@ -3,23 +3,26 @@ import { Request, Response, NextFunction } from 'express'; import { OpenViduMeetPermissions, OpenViduMeetRoom } from '@typings-ce'; import { LoggerService } from '../services/logger.service.js'; import { RoomService } from '../services/room.service.js'; +import { RecordingHelper } from '../helpers/recording.helper.js'; -export const withRecordingEnabledAndCorrectPermissions = async (req: Request, res: Response, next: NextFunction) => { +export const withRecordingEnabled = async (req: Request, res: Response, next: NextFunction) => { const logger = container.get(LoggerService); + const roomIdParam = req.body.roomId; + let roomId: string; - // TODO: Think how to get the roomName from the request - const roomName = req.body.roomName; - const payload = req.session?.tokenClaims; - - if (!payload) { - return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + // Extract roomId from body or from recordingId + if (roomIdParam) { + roomId = roomIdParam as string; + } else { + const recordingId = req.params.recordingId as string; + ({ roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId)); } let room: OpenViduMeetRoom; try { const roomService = container.get(RoomService); - room = await roomService.getOpenViduRoom(roomName); + room = await roomService.getOpenViduRoom(roomId); } catch (error) { logger.error('Error checking recording preferences:' + error); return res.status(403).json({ message: 'Recording is disabled in this room' }); @@ -37,7 +40,28 @@ export const withRecordingEnabledAndCorrectPermissions = async (req: Request, re return res.status(403).json({ message: 'Recording is disabled in this room' }); } - const sameRoom = payload.video?.room === roomName; + return next(); +}; + +export const withCorrectPermissions = async (req: Request, res: Response, next: NextFunction) => { + const roomIdParam = req.body.roomId; + let roomId: string; + + // Extract roomId from body or from recordingId + if (roomIdParam) { + roomId = roomIdParam as string; + } else { + const recordingId = req.params.recordingId as string; + ({ roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId)); + } + + const payload = req.session?.tokenClaims; + + if (!payload) { + return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); + } + + const sameRoom = payload.video?.room === roomId; const metadata = JSON.parse(payload.metadata || '{}'); const permissions = metadata.permissions as OpenViduMeetPermissions | undefined; const canRecord = permissions?.canRecord === true; diff --git a/backend/src/middlewares/room.middleware.ts b/backend/src/middlewares/room.middleware.ts new file mode 100644 index 0000000..7032435 --- /dev/null +++ b/backend/src/middlewares/room.middleware.ts @@ -0,0 +1,33 @@ +import { container } from '../config/dependency-injector.config.js'; +import { NextFunction, Request, Response } from 'express'; +import { LoggerService } from '../services/logger.service.js'; +import { GlobalPreferencesService } from '../services/index.js'; +import { allowAnonymous, apiKeyValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js'; +import { UserRole } from '@typings-ce'; + +export const configureRoomAuth = async (req: Request, res: Response, next: NextFunction) => { + const logger = container.get(LoggerService); + const globalPrefService = container.get(GlobalPreferencesService); + let allowRoomCreation: boolean; + let requireAuthentication: boolean; + + try { + const { securityPreferences } = await globalPrefService.getGlobalPreferences(); + ({ allowRoomCreation, requireAuthentication } = securityPreferences.roomCreationPolicy); + } catch (error) { + logger.error('Error checking room creation policy:' + error); + return res.status(500).json({ message: 'Internal server error' }); + } + + const authValidators = [apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)]; + + if (allowRoomCreation) { + if (requireAuthentication) { + authValidators.push(tokenAndRoleValidator(UserRole.USER)); + } else { + authValidators.push(allowAnonymous); + } + } + + return withAuth(...authValidators)(req, res, next); +}; diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 5079874..fff234c 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -3,8 +3,7 @@ import { Router } from 'express'; import bodyParser from 'body-parser'; import * as authCtrl from '../controllers/auth.controller.js'; import rateLimit from 'express-rate-limit'; -import { tokenAndRoleValidator, withAuth } from '../middlewares/auth.middleware.js'; -import { Role } from '@typings-ce'; +import { configureProfileAuth } from '../middlewares/auth.middleware.js'; import { validateLoginRequest } from '../middlewares/request-validators/auth-validator.middleware.js'; export const authRouter = Router(); @@ -23,8 +22,4 @@ authRouter.use(bodyParser.json()); authRouter.post('/login', validateLoginRequest, loginLimiter, authCtrl.login); authRouter.post('/logout', authCtrl.logout); authRouter.post('/refresh', authCtrl.refreshToken); -authRouter.get( - '/profile', - withAuth(tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)), - authCtrl.getProfile -); +authRouter.get('/profile', configureProfileAuth, authCtrl.getProfile); diff --git a/backend/src/routes/participants.routes.ts b/backend/src/routes/participants.routes.ts index 855b634..d193198 100644 --- a/backend/src/routes/participants.routes.ts +++ b/backend/src/routes/participants.routes.ts @@ -5,19 +5,29 @@ import { validateParticipantDeletionRequest, validateParticipantTokenRequest } from '../middlewares/request-validators/participant-validator.middleware.js'; +import { configureTokenAuth, withModeratorPermissions } from '../middlewares/participant.middleware.js'; +import { participantTokenValidator, withAuth } from '../middlewares/auth.middleware.js'; export const internalParticipantsRouter = Router(); internalParticipantsRouter.use(bodyParser.urlencoded({ extended: true })); internalParticipantsRouter.use(bodyParser.json()); -internalParticipantsRouter.post('/token', validateParticipantTokenRequest, participantCtrl.generateParticipantToken); +internalParticipantsRouter.post( + '/token', + validateParticipantTokenRequest, + configureTokenAuth, + participantCtrl.generateParticipantToken +); internalParticipantsRouter.post( '/token/refresh', validateParticipantTokenRequest, + configureTokenAuth, participantCtrl.refreshParticipantToken ); internalParticipantsRouter.delete( '/:participantName', + withAuth(participantTokenValidator), validateParticipantDeletionRequest, + withModeratorPermissions, participantCtrl.deleteParticipant ); diff --git a/backend/src/routes/recording.routes.ts b/backend/src/routes/recording.routes.ts index 207fa6f..6319151 100644 --- a/backend/src/routes/recording.routes.ts +++ b/backend/src/routes/recording.routes.ts @@ -1,16 +1,18 @@ import { Router } from 'express'; import bodyParser from 'body-parser'; import * as recordingCtrl from '../controllers/recording.controller.js'; -import { Role } from '@typings-ce'; +import { UserRole } from '@typings-ce'; import { withAuth, participantTokenValidator, tokenAndRoleValidator, - withRecordingEnabledAndCorrectPermissions, + withRecordingEnabled, + withCorrectPermissions, withValidGetRecordingsRequest, withValidRecordingBulkDeleteRequest, withValidRecordingId, - withValidStartRecordingRequest + withValidStartRecordingRequest, + apiKeyValidator } from '../middlewares/index.js'; export const recordingRouter = Router(); @@ -20,28 +22,44 @@ recordingRouter.use(bodyParser.json()); // Recording Routes recordingRouter.post( '/', - withAuth(participantTokenValidator), - withRecordingEnabledAndCorrectPermissions, withValidStartRecordingRequest, + withRecordingEnabled, + withAuth(participantTokenValidator), + withCorrectPermissions, recordingCtrl.startRecording ); recordingRouter.put( '/:recordingId', + withValidRecordingId, + withRecordingEnabled, withAuth(participantTokenValidator), - /* withRecordingEnabledAndCorrectPermissions,*/ withValidRecordingId, + withCorrectPermissions, recordingCtrl.stopRecording ); - recordingRouter.delete( '/:recordingId', - withAuth(tokenAndRoleValidator(Role.ADMIN), participantTokenValidator), - /*withRecordingEnabledAndCorrectPermissions,*/ withValidRecordingId, + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), recordingCtrl.deleteRecording ); -recordingRouter.get('/:recordingId', withValidRecordingId, recordingCtrl.getRecording); -recordingRouter.get('/', withValidGetRecordingsRequest, recordingCtrl.getRecordings); -recordingRouter.delete('/', withValidRecordingBulkDeleteRequest, recordingCtrl.bulkDeleteRecordings); +recordingRouter.get( + '/:recordingId', + withValidRecordingId, + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + recordingCtrl.getRecording +); +recordingRouter.get( + '/', + withValidGetRecordingsRequest, + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + recordingCtrl.getRecordings +); +recordingRouter.delete( + '/', + withValidRecordingBulkDeleteRequest, + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), + recordingCtrl.bulkDeleteRecordings +); // Internal Recording Routes export const internalRecordingRouter = Router(); @@ -50,7 +68,7 @@ internalRecordingRouter.use(bodyParser.json()); internalRecordingRouter.get( '/:recordingId/stream', - withAuth(participantTokenValidator), - /*withRecordingEnabledAndCorrectPermissions,*/ withValidRecordingId, + withValidRecordingId, + withAuth(tokenAndRoleValidator(UserRole.ADMIN)), recordingCtrl.streamRecording ); diff --git a/backend/src/routes/room.routes.ts b/backend/src/routes/room.routes.ts index 1a32140..1172734 100644 --- a/backend/src/routes/room.routes.ts +++ b/backend/src/routes/room.routes.ts @@ -6,7 +6,8 @@ import { validateGetRoomQueryParams, validateRoomRequest } from '../middlewares/request-validators/room-validator.middleware.js'; -import { Role } from '@typings-ce'; +import { UserRole } from '@typings-ce'; +import { configureRoomAuth } from '../middlewares/room.middleware.js'; export const roomRouter = Router(); @@ -14,25 +15,20 @@ roomRouter.use(bodyParser.urlencoded({ extended: true })); roomRouter.use(bodyParser.json()); // Room Routes -roomRouter.post( - '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)), - validateRoomRequest, - roomCtrl.createRoom -); +roomRouter.post('/', configureRoomAuth, validateRoomRequest, roomCtrl.createRoom); roomRouter.get( '/', - withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), validateGetRoomQueryParams, roomCtrl.getRooms ); roomRouter.get( '/:roomName', - withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)), + withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), tokenAndRoleValidator(UserRole.USER)), validateGetRoomQueryParams, roomCtrl.getRoom ); -roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), roomCtrl.deleteRooms); +roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms); // Room preferences -roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), roomCtrl.updateRoomPreferences); +roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences);