diff --git a/backend/src/config/@types/express/index.d.ts b/backend/src/config/@types/express/index.d.ts index 5cf4b31..ec0ad5a 100644 --- a/backend/src/config/@types/express/index.d.ts +++ b/backend/src/config/@types/express/index.d.ts @@ -1,5 +1,5 @@ +import { ParticipantRole, User } from '@typings-ce'; import { ClaimGrants } from 'livekit-server-sdk'; -import { User } from '@typings-ce'; // Override the Express Request type to include a session object with user and token properties // This will allow controllers to access the user and token information from the request object in a type-safe manner @@ -8,6 +8,7 @@ declare module 'express' { session?: { user?: User; tokenClaims?: ClaimGrants; + participantRole?: ParticipantRole; }; } } diff --git a/backend/src/config/internal-config.ts b/backend/src/config/internal-config.ts index ac2ceb9..f89562a 100644 --- a/backend/src/config/internal-config.ts +++ b/backend/src/config/internal-config.ts @@ -13,6 +13,7 @@ const INTERNAL_CONFIG = { // Headers for API requests API_KEY_HEADER: 'x-api-key', + PARTICIPANT_ROLE_HEADER: 'x-participant-role', // Authentication usernames ANONYMOUS_USER: 'anonymous', diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 8d8e7d1..bf22e33 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -1,4 +1,4 @@ -import { User, UserRole } from '@typings-ce'; +import { OpenViduMeetPermissions, ParticipantRole, User, UserRole } from '@typings-ce'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import rateLimit from 'express-rate-limit'; import { ClaimGrants } from 'livekit-server-sdk'; @@ -8,6 +8,7 @@ import INTERNAL_CONFIG from '../config/internal-config.js'; import { errorInsufficientPermissions, errorInvalidApiKey, + errorInvalidParticipantRole, errorInvalidToken, errorInvalidTokenSubject, errorUnauthorized, @@ -108,9 +109,10 @@ const validateTokenAndSetSession = async (req: Request, cookieName: string) => { } const tokenService = container.get(TokenService); + let payload: ClaimGrants; try { - const payload = await tokenService.verifyToken(token); + payload = await tokenService.verifyToken(token); const user = await getAuthenticatedUserOrAnonymous(req); req.session = req.session || {}; @@ -119,6 +121,31 @@ const validateTokenAndSetSession = async (req: Request, cookieName: string) => { } catch (error) { throw errorWithControl(errorInvalidToken(), true); } + + // If the token is a participant token, set the participant role in the session + if (cookieName === INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME) { + const participantRole = req.headers[INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER]; + const allRoles = [ParticipantRole.MODERATOR, ParticipantRole.PUBLISHER]; + + // Ensure the participant role is provided and valid + // This is required to distinguish roles when multiple are present in the token + if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) { + throw errorWithControl(errorInvalidParticipantRole(), true); + } + + // Check that the specified role is present in the token claims + const metadata = JSON.parse(payload.metadata || '{}'); + const roles = metadata.roles || []; + const hasRole = roles.some( + (r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === participantRole + ); + + if (!hasRole) { + throw errorWithControl(errorInsufficientPermissions(), true); + } + + req.session.participantRole = participantRole as ParticipantRole; + } }; // Configure API key validatior diff --git a/backend/src/middlewares/participant.middleware.ts b/backend/src/middlewares/participant.middleware.ts index a75a901..df31f87 100644 --- a/backend/src/middlewares/participant.middleware.ts +++ b/backend/src/middlewares/participant.middleware.ts @@ -56,15 +56,14 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response, export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { const { roomId } = req.params; const payload = req.session?.tokenClaims; + const role = req.session?.participantRole; - if (!payload) { + if (!payload || !role) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } const sameRoom = payload.video?.room === roomId; - const metadata = JSON.parse(payload.metadata || '{}'); - const role = metadata.role as ParticipantRole; if (!sameRoom || role !== ParticipantRole.MODERATOR) { const error = errorInsufficientPermissions(); diff --git a/backend/src/middlewares/recording.middleware.ts b/backend/src/middlewares/recording.middleware.ts index 031f832..6cb89d3 100644 --- a/backend/src/middlewares/recording.middleware.ts +++ b/backend/src/middlewares/recording.middleware.ts @@ -1,4 +1,4 @@ -import { MeetRoom, OpenViduMeetPermissions, RecordingPermissions, UserRole } from '@typings-ce'; +import { MeetRoom, OpenViduMeetPermissions, ParticipantRole, RecordingPermissions, UserRole } from '@typings-ce'; import { NextFunction, Request, Response } from 'express'; import { container } from '../config/index.js'; import { RecordingHelper } from '../helpers/index.js'; @@ -42,15 +42,18 @@ export const withRecordingEnabled = async (req: Request, res: Response, next: Ne export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => { const roomId = extractRoomIdFromRequest(req); const payload = req.session?.tokenClaims; + const role = req.session?.participantRole; - if (!payload) { + if (!payload || !role) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } const sameRoom = payload.video?.room === roomId; const metadata = JSON.parse(payload.metadata || '{}'); - const permissions = metadata.permissions as OpenViduMeetPermissions | undefined; + const permissions = metadata.roles?.find( + (r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === role + )?.permissions as OpenViduMeetPermissions | undefined; const canRecord = permissions?.canRecord; if (!sameRoom || !canRecord) { diff --git a/backend/src/middlewares/room.middleware.ts b/backend/src/middlewares/room.middleware.ts index 78a2700..b9d1f14 100644 --- a/backend/src/middlewares/room.middleware.ts +++ b/backend/src/middlewares/room.middleware.ts @@ -21,6 +21,7 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { const roomId = req.params.roomId as string; const payload = req.session?.tokenClaims; + const role = req.session?.participantRole; // If there is no token, the user is admin or it is invoked using the API key // In this case, the user is allowed to access the resource @@ -29,16 +30,10 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne } const sameRoom = payload.video?.room === roomId; - const metadata = JSON.parse(payload.metadata || '{}'); - const role = metadata.role as ParticipantRole; - if (!sameRoom) { - const error = errorInsufficientPermissions(); - return rejectRequestFromMeetError(res, error); - } - - // If the user is not a moderator, it is not allowed to access the resource - if (role !== ParticipantRole.MODERATOR) { + // If the user does not belong to the requested room, + // or the user is not a moderator, access is denied + if (!sameRoom || role !== ParticipantRole.MODERATOR) { const error = errorInsufficientPermissions(); return rejectRequestFromMeetError(res, error); } @@ -96,7 +91,8 @@ export const configureRecordingTokenAuth = async (req: Request, res: Response, n if (authModeToAccessRoom === AuthMode.NONE) { authValidators.push(allowAnonymous); } else { - const isModeratorsOnlyMode = authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === ParticipantRole.MODERATOR; + const isModeratorsOnlyMode = + authModeToAccessRoom === AuthMode.MODERATORS_ONLY && role === ParticipantRole.MODERATOR; const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS; if (isModeratorsOnlyMode || isAllUsersMode) {