backend: update participant token validation middleware to always check for participant role header to specified a valid role include in the token roles. Refactor related middlewares to use the new participant token structure

This commit is contained in:
juancarmore 2025-07-11 01:44:35 +02:00
parent 22ce0e7d66
commit fdd897b86b
6 changed files with 46 additions and 19 deletions

View File

@ -1,5 +1,5 @@
import { ParticipantRole, User } from '@typings-ce';
import { ClaimGrants } from 'livekit-server-sdk'; 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 // 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 // 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?: { session?: {
user?: User; user?: User;
tokenClaims?: ClaimGrants; tokenClaims?: ClaimGrants;
participantRole?: ParticipantRole;
}; };
} }
} }

View File

@ -13,6 +13,7 @@ const INTERNAL_CONFIG = {
// Headers for API requests // Headers for API requests
API_KEY_HEADER: 'x-api-key', API_KEY_HEADER: 'x-api-key',
PARTICIPANT_ROLE_HEADER: 'x-participant-role',
// Authentication usernames // Authentication usernames
ANONYMOUS_USER: 'anonymous', ANONYMOUS_USER: 'anonymous',

View File

@ -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 { NextFunction, Request, RequestHandler, Response } from 'express';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import { ClaimGrants } from 'livekit-server-sdk'; import { ClaimGrants } from 'livekit-server-sdk';
@ -8,6 +8,7 @@ import INTERNAL_CONFIG from '../config/internal-config.js';
import { import {
errorInsufficientPermissions, errorInsufficientPermissions,
errorInvalidApiKey, errorInvalidApiKey,
errorInvalidParticipantRole,
errorInvalidToken, errorInvalidToken,
errorInvalidTokenSubject, errorInvalidTokenSubject,
errorUnauthorized, errorUnauthorized,
@ -108,9 +109,10 @@ const validateTokenAndSetSession = async (req: Request, cookieName: string) => {
} }
const tokenService = container.get(TokenService); const tokenService = container.get(TokenService);
let payload: ClaimGrants;
try { try {
const payload = await tokenService.verifyToken(token); payload = await tokenService.verifyToken(token);
const user = await getAuthenticatedUserOrAnonymous(req); const user = await getAuthenticatedUserOrAnonymous(req);
req.session = req.session || {}; req.session = req.session || {};
@ -119,6 +121,31 @@ const validateTokenAndSetSession = async (req: Request, cookieName: string) => {
} catch (error) { } catch (error) {
throw errorWithControl(errorInvalidToken(), true); 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 // Configure API key validatior

View File

@ -56,15 +56,14 @@ export const configureParticipantTokenAuth = async (req: Request, res: Response,
export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => { export const withModeratorPermissions = async (req: Request, res: Response, next: NextFunction) => {
const { roomId } = req.params; const { roomId } = req.params;
const payload = req.session?.tokenClaims; const payload = req.session?.tokenClaims;
const role = req.session?.participantRole;
if (!payload) { if (!payload || !role) {
const error = errorInsufficientPermissions(); const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error); return rejectRequestFromMeetError(res, error);
} }
const sameRoom = payload.video?.room === roomId; const sameRoom = payload.video?.room === roomId;
const metadata = JSON.parse(payload.metadata || '{}');
const role = metadata.role as ParticipantRole;
if (!sameRoom || role !== ParticipantRole.MODERATOR) { if (!sameRoom || role !== ParticipantRole.MODERATOR) {
const error = errorInsufficientPermissions(); const error = errorInsufficientPermissions();

View File

@ -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 { NextFunction, Request, Response } from 'express';
import { container } from '../config/index.js'; import { container } from '../config/index.js';
import { RecordingHelper } from '../helpers/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) => { export const withCanRecordPermission = async (req: Request, res: Response, next: NextFunction) => {
const roomId = extractRoomIdFromRequest(req); const roomId = extractRoomIdFromRequest(req);
const payload = req.session?.tokenClaims; const payload = req.session?.tokenClaims;
const role = req.session?.participantRole;
if (!payload) { if (!payload || !role) {
const error = errorInsufficientPermissions(); const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error); return rejectRequestFromMeetError(res, error);
} }
const sameRoom = payload.video?.room === roomId; 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.roles?.find(
(r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === role
)?.permissions as OpenViduMeetPermissions | undefined;
const canRecord = permissions?.canRecord; const canRecord = permissions?.canRecord;
if (!sameRoom || !canRecord) { if (!sameRoom || !canRecord) {

View File

@ -21,6 +21,7 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa
export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => { export const configureRoomAuthorization = async (req: Request, res: Response, next: NextFunction) => {
const roomId = req.params.roomId as string; const roomId = req.params.roomId as string;
const payload = req.session?.tokenClaims; 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 // 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 // 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 sameRoom = payload.video?.room === roomId;
const metadata = JSON.parse(payload.metadata || '{}');
const role = metadata.role as ParticipantRole;
if (!sameRoom) { // If the user does not belong to the requested room,
const error = errorInsufficientPermissions(); // or the user is not a moderator, access is denied
return rejectRequestFromMeetError(res, error); if (!sameRoom || role !== ParticipantRole.MODERATOR) {
}
// If the user is not a moderator, it is not allowed to access the resource
if (role !== ParticipantRole.MODERATOR) {
const error = errorInsufficientPermissions(); const error = errorInsufficientPermissions();
return rejectRequestFromMeetError(res, error); return rejectRequestFromMeetError(res, error);
} }
@ -96,7 +91,8 @@ export const configureRecordingTokenAuth = async (req: Request, res: Response, n
if (authModeToAccessRoom === AuthMode.NONE) { if (authModeToAccessRoom === AuthMode.NONE) {
authValidators.push(allowAnonymous); authValidators.push(allowAnonymous);
} else { } 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; const isAllUsersMode = authModeToAccessRoom === AuthMode.ALL_USERS;
if (isModeratorsOnlyMode || isAllUsersMode) { if (isModeratorsOnlyMode || isAllUsersMode) {