backend: enhance token validation with new error handling and metadata parsing for participants and recordings
This commit is contained in:
parent
972f6f4f90
commit
94b98600ba
@ -1,4 +1,4 @@
|
||||
import { OpenViduMeetPermissions, ParticipantRole, User, UserRole } from '@typings-ce';
|
||||
import { MeetTokenMetadata, 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';
|
||||
@ -9,6 +9,8 @@ import {
|
||||
errorInsufficientPermissions,
|
||||
errorInvalidApiKey,
|
||||
errorInvalidParticipantRole,
|
||||
errorInvalidParticipantToken,
|
||||
errorInvalidRecordingToken,
|
||||
errorInvalidToken,
|
||||
errorInvalidTokenSubject,
|
||||
errorUnauthorized,
|
||||
@ -16,7 +18,14 @@ import {
|
||||
OpenViduMeetError,
|
||||
rejectRequestFromMeetError
|
||||
} from '../models/index.js';
|
||||
import { AuthService, LoggerService, TokenService, UserService } from '../services/index.js';
|
||||
import {
|
||||
AuthService,
|
||||
LoggerService,
|
||||
ParticipantService,
|
||||
RoomService,
|
||||
TokenService,
|
||||
UserService
|
||||
} from '../services/index.js';
|
||||
|
||||
/**
|
||||
* This middleware allows to chain multiple validators to check if the request is authorized.
|
||||
@ -104,13 +113,19 @@ export const participantTokenValidator = async (req: Request) => {
|
||||
throw errorWithControl(errorInvalidParticipantRole(), true);
|
||||
}
|
||||
|
||||
if (!participantRole || !allRoles.includes(participantRole as ParticipantRole)) {
|
||||
throw errorWithControl(errorInvalidParticipantRole(), true);
|
||||
// Check that the specified role is present in the token claims
|
||||
let metadata: MeetTokenMetadata;
|
||||
|
||||
try {
|
||||
const participantService = container.get(ParticipantService);
|
||||
metadata = participantService.parseMetadata(req.session?.tokenClaims?.metadata || '{}');
|
||||
} catch (error) {
|
||||
const logger = container.get(LoggerService);
|
||||
logger.error('Invalid participant token:', error);
|
||||
throw errorWithControl(errorInvalidParticipantToken(), true);
|
||||
}
|
||||
|
||||
// Check that the specified role is present in the token claims
|
||||
const metadata = JSON.parse(req.session?.tokenClaims?.metadata || '{}');
|
||||
const roles = metadata.roles || [];
|
||||
const roles = metadata.roles;
|
||||
const hasRole = roles.some(
|
||||
(r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === participantRole
|
||||
);
|
||||
@ -126,6 +141,16 @@ export const participantTokenValidator = async (req: Request) => {
|
||||
// Configure token validator for recording access
|
||||
export const recordingTokenValidator = async (req: Request) => {
|
||||
await validateTokenAndSetSession(req, INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME);
|
||||
|
||||
// Validate the recording token metadata
|
||||
try {
|
||||
const roomService = container.get(RoomService);
|
||||
roomService.parseRecordingTokenMetadata(req.session?.tokenClaims?.metadata || '{}');
|
||||
} catch (error) {
|
||||
const logger = container.get(LoggerService);
|
||||
logger.error('Invalid recording token:', error);
|
||||
throw errorWithControl(errorInvalidRecordingToken(), true);
|
||||
}
|
||||
};
|
||||
|
||||
const validateTokenAndSetSession = async (req: Request, cookieName: string) => {
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@typings-ce';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/index.js';
|
||||
import {
|
||||
errorInsufficientPermissions,
|
||||
errorInvalidParticipantRole,
|
||||
handleError,
|
||||
rejectRequestFromMeetError
|
||||
} from '../models/error.model.js';
|
||||
import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
|
||||
import { MeetStorageService, RoomService } from '../services/index.js';
|
||||
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
||||
|
||||
@ -96,22 +91,3 @@ export const checkParticipantFromSameRoom = async (req: Request, res: Response,
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const withValidParticipantRole = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { role } = req.body;
|
||||
|
||||
if (!role) {
|
||||
const error = errorInvalidParticipantRole();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
// Validate the role against the ParticipantRole enum
|
||||
const isRoleValid = role === ParticipantRole.MODERATOR || role === ParticipantRole.SPEAKER;
|
||||
|
||||
if (!isRoleValid) {
|
||||
const error = errorInvalidParticipantRole();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MeetRoom, OpenViduMeetPermissions, ParticipantRole, RecordingPermissions, UserRole } from '@typings-ce';
|
||||
import { MeetRoom, UserRole } from '@typings-ce';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/index.js';
|
||||
import { RecordingHelper } from '../helpers/index.js';
|
||||
@ -10,7 +10,7 @@ import {
|
||||
handleError,
|
||||
rejectRequestFromMeetError
|
||||
} from '../models/error.model.js';
|
||||
import { LoggerService, MeetStorageService, RoomService } from '../services/index.js';
|
||||
import { LoggerService, MeetStorageService, ParticipantService, RoomService } from '../services/index.js';
|
||||
import {
|
||||
allowAnonymous,
|
||||
apiKeyValidator,
|
||||
@ -49,11 +49,11 @@ export const withCanRecordPermission = async (req: Request, res: Response, next:
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
const participantService = container.get(ParticipantService);
|
||||
const metadata = participantService.parseMetadata(payload.metadata || '{}');
|
||||
|
||||
const sameRoom = payload.video?.room === roomId;
|
||||
const metadata = JSON.parse(payload.metadata || '{}');
|
||||
const permissions = metadata.roles?.find(
|
||||
(r: { role: ParticipantRole; permissions: OpenViduMeetPermissions }) => r.role === role
|
||||
)?.permissions as OpenViduMeetPermissions | undefined;
|
||||
const permissions = metadata.roles.find((r) => r.role === role)?.permissions;
|
||||
const canRecord = permissions?.canRecord;
|
||||
|
||||
if (!sameRoom || !canRecord) {
|
||||
@ -80,10 +80,11 @@ export const withCanRetrieveRecordingsPermission = async (req: Request, res: Res
|
||||
return next();
|
||||
}
|
||||
|
||||
const roomService = container.get(RoomService);
|
||||
const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}');
|
||||
|
||||
const sameRoom = roomId ? payload.video?.room === roomId : true;
|
||||
const metadata = JSON.parse(payload.metadata || '{}');
|
||||
const permissions = metadata.recordingPermissions as RecordingPermissions | undefined;
|
||||
const canRetrieveRecordings = permissions?.canRetrieveRecordings;
|
||||
const canRetrieveRecordings = metadata.recordingPermissions.canRetrieveRecordings;
|
||||
|
||||
if (!sameRoom || !canRetrieveRecordings) {
|
||||
const error = errorInsufficientPermissions();
|
||||
@ -103,10 +104,11 @@ export const withCanDeleteRecordingsPermission = async (req: Request, res: Respo
|
||||
return next();
|
||||
}
|
||||
|
||||
const roomService = container.get(RoomService);
|
||||
const metadata = roomService.parseRecordingTokenMetadata(payload.metadata || '{}');
|
||||
|
||||
const sameRoom = roomId ? payload.video?.room === roomId : true;
|
||||
const metadata = JSON.parse(payload.metadata || '{}');
|
||||
const permissions = metadata.recordingPermissions as RecordingPermissions | undefined;
|
||||
const canDeleteRecordings = permissions?.canDeleteRecordings;
|
||||
const canDeleteRecordings = metadata.recordingPermissions.canDeleteRecordings;
|
||||
|
||||
if (!sameRoom || !canDeleteRecordings) {
|
||||
const error = errorInsufficientPermissions();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ParticipantOptions } from '@typings-ce';
|
||||
import { MeetTokenMetadata, OpenViduMeetPermissions, ParticipantOptions, ParticipantRole } from '@typings-ce';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { rejectUnprocessableRequest } from '../../models/error.model.js';
|
||||
@ -10,6 +10,27 @@ const ParticipantTokenRequestSchema: z.ZodType<ParticipantOptions> = z.object({
|
||||
participantName: z.string().optional()
|
||||
});
|
||||
|
||||
const UpdateParticipantRequestSchema = z.object({
|
||||
role: z.enum([ParticipantRole.MODERATOR, ParticipantRole.SPEAKER])
|
||||
});
|
||||
|
||||
const OpenViduMeetPermissionsSchema: z.ZodType<OpenViduMeetPermissions> = z.object({
|
||||
canRecord: z.boolean(),
|
||||
canChat: z.boolean(),
|
||||
canChangeVirtualBackground: z.boolean()
|
||||
});
|
||||
|
||||
const MeetTokenMetadataSchema: z.ZodType<MeetTokenMetadata> = z.object({
|
||||
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
role: z.enum([ParticipantRole.MODERATOR, ParticipantRole.SPEAKER]),
|
||||
permissions: OpenViduMeetPermissionsSchema
|
||||
})
|
||||
),
|
||||
selectedRole: z.enum([ParticipantRole.MODERATOR, ParticipantRole.SPEAKER])
|
||||
});
|
||||
|
||||
export const validateParticipantTokenRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||
const { success, error, data } = ParticipantTokenRequestSchema.safeParse(req.body);
|
||||
|
||||
@ -20,3 +41,24 @@ export const validateParticipantTokenRequest = (req: Request, res: Response, nex
|
||||
req.body = data;
|
||||
next();
|
||||
};
|
||||
|
||||
export const validateUpdateParticipantRequest = (req: Request, res: Response, next: NextFunction) => {
|
||||
const { success, error, data } = UpdateParticipantRequestSchema.safeParse(req.body);
|
||||
|
||||
if (!success) {
|
||||
return rejectUnprocessableRequest(res, error);
|
||||
}
|
||||
|
||||
req.body = data;
|
||||
next();
|
||||
};
|
||||
|
||||
export const validateMeetTokenMetadata = (metadata: unknown): MeetTokenMetadata => {
|
||||
const { success, error, data } = MeetTokenMetadataSchema.safeParse(metadata);
|
||||
|
||||
if (!success) {
|
||||
throw new Error(`Invalid metadata: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@ -5,7 +5,9 @@ import {
|
||||
MeetRoomFilters,
|
||||
MeetRoomOptions,
|
||||
MeetRoomPreferences,
|
||||
MeetVirtualBackgroundPreferences
|
||||
MeetVirtualBackgroundPreferences,
|
||||
ParticipantRole,
|
||||
RecordingPermissions
|
||||
} from '@typings-ce';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import ms from 'ms';
|
||||
@ -185,6 +187,16 @@ const RecordingTokenRequestSchema = z.object({
|
||||
secret: z.string().nonempty('Secret is required')
|
||||
});
|
||||
|
||||
const RecordingPermissionsSchema: z.ZodType<RecordingPermissions> = z.object({
|
||||
canRetrieveRecordings: z.boolean(),
|
||||
canDeleteRecordings: z.boolean()
|
||||
});
|
||||
|
||||
const RecordingTokenMetadataSchema = z.object({
|
||||
role: z.enum([ParticipantRole.MODERATOR, ParticipantRole.SPEAKER]),
|
||||
recordingPermissions: RecordingPermissionsSchema
|
||||
});
|
||||
|
||||
export const withValidRoomOptions = (req: Request, res: Response, next: NextFunction) => {
|
||||
const { success, error, data } = RoomRequestOptionsSchema.safeParse(req.body);
|
||||
|
||||
@ -192,7 +204,6 @@ export const withValidRoomOptions = (req: Request, res: Response, next: NextFunc
|
||||
return rejectUnprocessableRequest(res, error);
|
||||
}
|
||||
|
||||
console.log('VALID ROOM OPTIONS', data);
|
||||
req.body = data;
|
||||
next();
|
||||
};
|
||||
@ -275,3 +286,13 @@ export const withValidRoomSecret = (req: Request, res: Response, next: NextFunct
|
||||
req.body = data;
|
||||
next();
|
||||
};
|
||||
|
||||
export const validateRecordingTokenMetadata = (metadata: unknown) => {
|
||||
const { success, error, data } = RecordingTokenMetadataSchema.safeParse(metadata);
|
||||
|
||||
if (!success) {
|
||||
throw new Error(`Invalid metadata: ${error.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@ -166,6 +166,10 @@ export const errorRecordingsNotFromSameRoom = (roomId: string): OpenViduMeetErro
|
||||
);
|
||||
};
|
||||
|
||||
export const errorInvalidRecordingToken = (): OpenViduMeetError => {
|
||||
return new OpenViduMeetError('Recording', 'Invalid recording token', 400);
|
||||
};
|
||||
|
||||
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
|
||||
return (
|
||||
error instanceof OpenViduMeetError &&
|
||||
@ -210,18 +214,18 @@ export const errorInvalidRoomSecret = (roomId: string, secret: string): OpenVidu
|
||||
|
||||
// Participant errors
|
||||
|
||||
export const errorParticipantNotFound = (participantName: string, roomId: string): OpenViduMeetError => {
|
||||
export const errorParticipantNotFound = (participantIdentity: string, roomId: string): OpenViduMeetError => {
|
||||
return new OpenViduMeetError(
|
||||
'Participant Error',
|
||||
`Participant '${participantName}' not found in room '${roomId}'`,
|
||||
`Participant '${participantIdentity}' not found in room '${roomId}'`,
|
||||
404
|
||||
);
|
||||
};
|
||||
|
||||
export const errorParticipantAlreadyExists = (participantName: string, roomId: string): OpenViduMeetError => {
|
||||
export const errorParticipantAlreadyExists = (participantIdentity: string, roomId: string): OpenViduMeetError => {
|
||||
return new OpenViduMeetError(
|
||||
'Participant Error',
|
||||
`Participant '${participantName}' already exists in room '${roomId}'`,
|
||||
`Participant '${participantIdentity}' already exists in room '${roomId}'`,
|
||||
409
|
||||
);
|
||||
};
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
TokenService,
|
||||
FrontendEventService
|
||||
} from './index.js';
|
||||
import { validateRecordingTokenMetadata } from '../middlewares/index.js';
|
||||
|
||||
/**
|
||||
* Service for managing OpenVidu Meet rooms.
|
||||
@ -311,6 +312,16 @@ export class RoomService {
|
||||
};
|
||||
}
|
||||
|
||||
parseRecordingTokenMetadata(metadata: string) {
|
||||
try {
|
||||
const parsedMetadata = JSON.parse(metadata);
|
||||
return validateRecordingTokenMetadata(parsedMetadata);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to parse recording token metadata:', error);
|
||||
throw new Error('Invalid recording token metadata format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies rooms into those that should be deleted immediately vs marked for deletion
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user