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:
parent
22ce0e7d66
commit
fdd897b86b
3
backend/src/config/@types/express/index.d.ts
vendored
3
backend/src/config/@types/express/index.d.ts
vendored
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user