backend: Refactor middlewares and routes to configure authentication

This commit is contained in:
juancarmore 2025-03-25 13:10:08 +01:00
parent 042f7f2fd4
commit 147a334868
9 changed files with 222 additions and 42 deletions

View File

@ -104,3 +104,38 @@ export const apiKeyValidator = async (req: Request) => {
throw errorInvalidApiKey(); 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);
};

View File

@ -1,4 +1,6 @@
export * from './auth.middleware.js'; export * from './auth.middleware.js';
export * from './room.middleware.js';
export * from './participant.middleware.js';
export * from './recording.middleware.js'; export * from './recording.middleware.js';
export * from './content-type.middleware.js'; export * from './content-type.middleware.js';
export * from './request-validators/participant-validator.middleware.js'; export * from './request-validators/participant-validator.middleware.js';

View File

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

View File

@ -3,23 +3,26 @@ import { Request, Response, NextFunction } from 'express';
import { OpenViduMeetPermissions, OpenViduMeetRoom } from '@typings-ce'; import { OpenViduMeetPermissions, OpenViduMeetRoom } from '@typings-ce';
import { LoggerService } from '../services/logger.service.js'; import { LoggerService } from '../services/logger.service.js';
import { RoomService } from '../services/room.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 logger = container.get(LoggerService);
const roomIdParam = req.body.roomId;
let roomId: string;
// TODO: Think how to get the roomName from the request // Extract roomId from body or from recordingId
const roomName = req.body.roomName; if (roomIdParam) {
const payload = req.session?.tokenClaims; roomId = roomIdParam as string;
} else {
if (!payload) { const recordingId = req.params.recordingId as string;
return res.status(403).json({ message: 'Insufficient permissions to access this resource' }); ({ roomId } = RecordingHelper.extractInfoFromRecordingId(recordingId));
} }
let room: OpenViduMeetRoom; let room: OpenViduMeetRoom;
try { try {
const roomService = container.get(RoomService); const roomService = container.get(RoomService);
room = await roomService.getOpenViduRoom(roomName); room = await roomService.getOpenViduRoom(roomId);
} catch (error) { } catch (error) {
logger.error('Error checking recording preferences:' + error); logger.error('Error checking recording preferences:' + error);
return res.status(403).json({ message: 'Recording is disabled in this room' }); 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' }); 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 metadata = JSON.parse(payload.metadata || '{}');
const permissions = metadata.permissions as OpenViduMeetPermissions | undefined; const permissions = metadata.permissions as OpenViduMeetPermissions | undefined;
const canRecord = permissions?.canRecord === true; const canRecord = permissions?.canRecord === true;

View File

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

View File

@ -3,8 +3,7 @@ import { Router } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import * as authCtrl from '../controllers/auth.controller.js'; import * as authCtrl from '../controllers/auth.controller.js';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import { tokenAndRoleValidator, withAuth } from '../middlewares/auth.middleware.js'; import { configureProfileAuth } from '../middlewares/auth.middleware.js';
import { Role } from '@typings-ce';
import { validateLoginRequest } from '../middlewares/request-validators/auth-validator.middleware.js'; import { validateLoginRequest } from '../middlewares/request-validators/auth-validator.middleware.js';
export const authRouter = Router(); export const authRouter = Router();
@ -23,8 +22,4 @@ authRouter.use(bodyParser.json());
authRouter.post('/login', validateLoginRequest, loginLimiter, authCtrl.login); authRouter.post('/login', validateLoginRequest, loginLimiter, authCtrl.login);
authRouter.post('/logout', authCtrl.logout); authRouter.post('/logout', authCtrl.logout);
authRouter.post('/refresh', authCtrl.refreshToken); authRouter.post('/refresh', authCtrl.refreshToken);
authRouter.get( authRouter.get('/profile', configureProfileAuth, authCtrl.getProfile);
'/profile',
withAuth(tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)),
authCtrl.getProfile
);

View File

@ -5,19 +5,29 @@ import {
validateParticipantDeletionRequest, validateParticipantDeletionRequest,
validateParticipantTokenRequest validateParticipantTokenRequest
} from '../middlewares/request-validators/participant-validator.middleware.js'; } 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(); export const internalParticipantsRouter = Router();
internalParticipantsRouter.use(bodyParser.urlencoded({ extended: true })); internalParticipantsRouter.use(bodyParser.urlencoded({ extended: true }));
internalParticipantsRouter.use(bodyParser.json()); internalParticipantsRouter.use(bodyParser.json());
internalParticipantsRouter.post('/token', validateParticipantTokenRequest, participantCtrl.generateParticipantToken); internalParticipantsRouter.post(
'/token',
validateParticipantTokenRequest,
configureTokenAuth,
participantCtrl.generateParticipantToken
);
internalParticipantsRouter.post( internalParticipantsRouter.post(
'/token/refresh', '/token/refresh',
validateParticipantTokenRequest, validateParticipantTokenRequest,
configureTokenAuth,
participantCtrl.refreshParticipantToken participantCtrl.refreshParticipantToken
); );
internalParticipantsRouter.delete( internalParticipantsRouter.delete(
'/:participantName', '/:participantName',
withAuth(participantTokenValidator),
validateParticipantDeletionRequest, validateParticipantDeletionRequest,
withModeratorPermissions,
participantCtrl.deleteParticipant participantCtrl.deleteParticipant
); );

View File

@ -1,16 +1,18 @@
import { Router } from 'express'; import { Router } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import * as recordingCtrl from '../controllers/recording.controller.js'; import * as recordingCtrl from '../controllers/recording.controller.js';
import { Role } from '@typings-ce'; import { UserRole } from '@typings-ce';
import { import {
withAuth, withAuth,
participantTokenValidator, participantTokenValidator,
tokenAndRoleValidator, tokenAndRoleValidator,
withRecordingEnabledAndCorrectPermissions, withRecordingEnabled,
withCorrectPermissions,
withValidGetRecordingsRequest, withValidGetRecordingsRequest,
withValidRecordingBulkDeleteRequest, withValidRecordingBulkDeleteRequest,
withValidRecordingId, withValidRecordingId,
withValidStartRecordingRequest withValidStartRecordingRequest,
apiKeyValidator
} from '../middlewares/index.js'; } from '../middlewares/index.js';
export const recordingRouter = Router(); export const recordingRouter = Router();
@ -20,28 +22,44 @@ recordingRouter.use(bodyParser.json());
// Recording Routes // Recording Routes
recordingRouter.post( recordingRouter.post(
'/', '/',
withAuth(participantTokenValidator),
withRecordingEnabledAndCorrectPermissions,
withValidStartRecordingRequest, withValidStartRecordingRequest,
withRecordingEnabled,
withAuth(participantTokenValidator),
withCorrectPermissions,
recordingCtrl.startRecording recordingCtrl.startRecording
); );
recordingRouter.put( recordingRouter.put(
'/:recordingId', '/:recordingId',
withValidRecordingId,
withRecordingEnabled,
withAuth(participantTokenValidator), withAuth(participantTokenValidator),
/* withRecordingEnabledAndCorrectPermissions,*/ withValidRecordingId, withCorrectPermissions,
recordingCtrl.stopRecording recordingCtrl.stopRecording
); );
recordingRouter.delete( recordingRouter.delete(
'/:recordingId', '/:recordingId',
withAuth(tokenAndRoleValidator(Role.ADMIN), participantTokenValidator),
/*withRecordingEnabledAndCorrectPermissions,*/
withValidRecordingId, withValidRecordingId,
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
recordingCtrl.deleteRecording recordingCtrl.deleteRecording
); );
recordingRouter.get('/:recordingId', withValidRecordingId, recordingCtrl.getRecording); recordingRouter.get(
recordingRouter.get('/', withValidGetRecordingsRequest, recordingCtrl.getRecordings); '/:recordingId',
recordingRouter.delete('/', withValidRecordingBulkDeleteRequest, recordingCtrl.bulkDeleteRecordings); 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 // Internal Recording Routes
export const internalRecordingRouter = Router(); export const internalRecordingRouter = Router();
@ -50,7 +68,7 @@ internalRecordingRouter.use(bodyParser.json());
internalRecordingRouter.get( internalRecordingRouter.get(
'/:recordingId/stream', '/:recordingId/stream',
withAuth(participantTokenValidator), withValidRecordingId,
/*withRecordingEnabledAndCorrectPermissions,*/ withValidRecordingId, withAuth(tokenAndRoleValidator(UserRole.ADMIN)),
recordingCtrl.streamRecording recordingCtrl.streamRecording
); );

View File

@ -6,7 +6,8 @@ import {
validateGetRoomQueryParams, validateGetRoomQueryParams,
validateRoomRequest validateRoomRequest
} from '../middlewares/request-validators/room-validator.middleware.js'; } 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(); export const roomRouter = Router();
@ -14,25 +15,20 @@ roomRouter.use(bodyParser.urlencoded({ extended: true }));
roomRouter.use(bodyParser.json()); roomRouter.use(bodyParser.json());
// Room Routes // Room Routes
roomRouter.post( roomRouter.post('/', configureRoomAuth, validateRoomRequest, roomCtrl.createRoom);
'/',
withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)),
validateRoomRequest,
roomCtrl.createRoom
);
roomRouter.get( roomRouter.get(
'/', '/',
withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
validateGetRoomQueryParams, validateGetRoomQueryParams,
roomCtrl.getRooms roomCtrl.getRooms
); );
roomRouter.get( roomRouter.get(
'/:roomName', '/:roomName',
withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN), tokenAndRoleValidator(Role.USER)), withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN), tokenAndRoleValidator(UserRole.USER)),
validateGetRoomQueryParams, validateGetRoomQueryParams,
roomCtrl.getRoom roomCtrl.getRoom
); );
roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), roomCtrl.deleteRooms); roomRouter.delete('/:roomName', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.deleteRooms);
// Room preferences // Room preferences
roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(Role.ADMIN)), roomCtrl.updateRoomPreferences); roomRouter.put('/', withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)), roomCtrl.updateRoomPreferences);