diff --git a/meet-ce/backend/src/config/internal-config.ts b/meet-ce/backend/src/config/internal-config.ts index d7b95776..b2b78481 100644 --- a/meet-ce/backend/src/config/internal-config.ts +++ b/meet-ce/backend/src/config/internal-config.ts @@ -17,10 +17,6 @@ export const INTERNAL_CONFIG = { REFRESH_TOKEN_EXPIRATION: '1d', ROOM_MEMBER_TOKEN_EXPIRATION: '2h', - // Authentication usernames - ANONYMOUS_USER: 'anonymous', - API_USER: 'api-user', - // S3 configuration S3_MAX_RETRIES_ATTEMPTS_ON_SAVE_ERROR: '5', S3_INITIAL_RETRY_DELAY_MS: '100', diff --git a/meet-ce/backend/src/middlewares/auth.middleware.ts b/meet-ce/backend/src/middlewares/auth.middleware.ts index 01945e7d..fec84a43 100644 --- a/meet-ce/backend/src/middlewares/auth.middleware.ts +++ b/meet-ce/backend/src/middlewares/auth.middleware.ts @@ -1,4 +1,4 @@ -import { LiveKitPermissions, MeetUser, MeetUserRole } from '@openvidu-meet/typings'; +import { MeetUser, MeetUserRole } from '@openvidu-meet/typings'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import rateLimit from 'express-rate-limit'; import { ClaimGrants } from 'livekit-server-sdk'; @@ -9,6 +9,7 @@ import { OpenViduMeetError, errorInsufficientPermissions, errorInvalidApiKey, + errorInvalidApiKeySubject, errorInvalidRoomMemberToken, errorInvalidToken, errorInvalidTokenSubject, @@ -27,7 +28,7 @@ import { getAccessToken, getRoomMemberToken } from '../utils/token.utils.js'; * Interface for authentication validators. * Each validator must implement methods to check if credentials are present and to validate them. */ -interface AuthValidator { +export interface AuthValidator { /** * Checks if the authentication credentials for this validator are present in the request. * This allows the middleware to skip validation for methods that are not being used. @@ -74,7 +75,7 @@ export const withAuth = (...validators: AuthValidator[]): RequestHandler => { /** * Token and role validator for role-based access. - * Validates JWT tokens and checks if the user has at least one of the required roles. + * Validates JWT tokens and checks if the user has one of the required roles. * * @param roles One or more roles that are allowed to access the resource */ @@ -92,27 +93,25 @@ export const tokenAndRoleValidator = (...roles: MeetUserRole[]): AuthValidator = throw errorUnauthorized(); } - const tokenService = container.get(TokenService); let payload: ClaimGrants; try { + const tokenService = container.get(TokenService); payload = await tokenService.verifyToken(token); } catch (error) { throw errorInvalidToken(); } - const username = payload.sub; const userService = container.get(UserService); - const user = username ? await userService.getUser(username) : null; + const userId = payload.sub; + const user = userId ? await userService.getUser(userId) : null; if (!user) { throw errorInvalidTokenSubject(); } - // Check if user has at least one of the required roles - const hasRequiredRole = roles.some((role) => user.roles.includes(role)); - - if (!hasRequiredRole) { + // Check if user has one of the required roles + if (!roles.includes(user.role)) { throw errorInsufficientPermissions(); } @@ -124,7 +123,7 @@ export const tokenAndRoleValidator = (...roles: MeetUserRole[]): AuthValidator = /** * Room member token validator for room access. - * Validates room member tokens and checks role permissions. + * Validates room member tokens and sets the room member metadata in the session. */ export const roomMemberTokenValidator: AuthValidator = { async isPresent(req: Request): Promise { @@ -140,13 +139,12 @@ export const roomMemberTokenValidator: AuthValidator = { } let tokenMetadata: string | undefined; - let livekitPermissions: LiveKitPermissions | undefined; try { const tokenService = container.get(TokenService); - ({ metadata: tokenMetadata, video: livekitPermissions } = await tokenService.verifyToken(token)); + ({ metadata: tokenMetadata } = await tokenService.verifyToken(token)); - if (!tokenMetadata || !livekitPermissions) { + if (!tokenMetadata) { throw new Error('Missing required token claims'); } } catch (error) { @@ -155,22 +153,23 @@ export const roomMemberTokenValidator: AuthValidator = { const requestSessionService = container.get(RequestSessionService); - // Validate the room member token metadata and extract role and permissions + // Validate the room member token metadata and set it in the session try { const roomMemberService = container.get(RoomMemberService); - const { role, permissions: meetPermissions } = - roomMemberService.parseRoomMemberTokenMetadata(tokenMetadata); - - requestSessionService.setRoomMemberTokenInfo(role, meetPermissions, livekitPermissions); + const parsedMetadata = roomMemberService.parseRoomMemberTokenMetadata(tokenMetadata); + requestSessionService.setRoomMemberTokenMetadata(parsedMetadata); } catch (error) { const logger = container.get(LoggerService); logger.error('Invalid room member token:', error); throw errorInvalidRoomMemberToken(); } - // Set authenticated user if present, otherwise anonymous + // Set authenticated user if present const user = await getAuthenticatedUserOrAnonymous(req); - requestSessionService.setUser(user); + + if (user) { + requestSessionService.setUser(user); + } } }; @@ -199,7 +198,11 @@ export const apiKeyValidator: AuthValidator = { } const userService = container.get(UserService); - const apiUser = userService.getApiUser(); + const apiUser = await userService.getUserAssociatedWithApiKey(); + + if (!apiUser) { + throw errorInvalidApiKeySubject(); + } const requestSessionService = container.get(RequestSessionService); requestSessionService.setUser(apiUser); @@ -208,7 +211,7 @@ export const apiKeyValidator: AuthValidator = { /** * Anonymous access validator. - * Always present and allows unauthenticated access with an anonymous user. + * Allows unauthenticated access with an anonymous user. */ export const allowAnonymous: AuthValidator = { async isPresent(): Promise { @@ -219,14 +222,15 @@ export const allowAnonymous: AuthValidator = { async validate(req: Request): Promise { const user = await getAuthenticatedUserOrAnonymous(req); - const requestSessionService = container.get(RequestSessionService); - requestSessionService.setUser(user); + if (user) { + const requestSessionService = container.get(RequestSessionService); + requestSessionService.setUser(user); + } } }; -// Return the authenticated user if available, otherwise return an anonymous user -const getAuthenticatedUserOrAnonymous = async (req: Request): Promise => { - const userService = container.get(UserService); +// Return the authenticated user if available, otherwise return null +const getAuthenticatedUserOrAnonymous = async (req: Request): Promise => { let user: MeetUser | null = null; // Check if there is a user already authenticated @@ -236,18 +240,16 @@ const getAuthenticatedUserOrAnonymous = async (req: Request): Promise try { const tokenService = container.get(TokenService); const payload = await tokenService.verifyToken(token); - const username = payload.sub; - user = username ? await userService.getUser(username) : null; + const userId = payload.sub; + + const userService = container.get(UserService); + user = userId ? await userService.getUser(userId) : null; } catch (error) { const logger = container.get(LoggerService); logger.debug('Token found but invalid:' + error); } } - if (!user) { - user = userService.getAnonymousUser(); - } - return user; }; diff --git a/meet-ce/backend/src/models/error.model.ts b/meet-ce/backend/src/models/error.model.ts index 8df30b85..dbc94058 100644 --- a/meet-ce/backend/src/models/error.model.ts +++ b/meet-ce/backend/src/models/error.model.ts @@ -101,6 +101,14 @@ export const errorInvalidApiKey = (): OpenViduMeetError => { return new OpenViduMeetError('Authentication Error', 'Invalid API key', 401); }; +export const errorInvalidApiKeySubject = (): OpenViduMeetError => { + return new OpenViduMeetError( + 'Authorization Error', + 'Invalid API key subject. The user associated with the API key does not exist', + 403 + ); +}; + export const errorApiKeyNotConfigured = (): OpenViduMeetError => { return new OpenViduMeetError( 'Authentication Error', diff --git a/meet-ce/backend/src/models/request-context.model.ts b/meet-ce/backend/src/models/request-context.model.ts index ca07daa3..81dc20bc 100644 --- a/meet-ce/backend/src/models/request-context.model.ts +++ b/meet-ce/backend/src/models/request-context.model.ts @@ -1,9 +1,9 @@ -import { MeetRoomMemberRoleAndPermissions, MeetUser } from '@openvidu-meet/typings'; +import { MeetRoomMemberTokenMetadata, MeetUser } from '@openvidu-meet/typings'; /** * Context information stored per HTTP request. */ export interface RequestContext { user?: MeetUser; - roomMember?: MeetRoomMemberRoleAndPermissions; + roomMember?: MeetRoomMemberTokenMetadata; } diff --git a/meet-ce/backend/src/services/request-session.service.ts b/meet-ce/backend/src/services/request-session.service.ts index fa9f7de4..62ad781d 100644 --- a/meet-ce/backend/src/services/request-session.service.ts +++ b/meet-ce/backend/src/services/request-session.service.ts @@ -1,4 +1,9 @@ -import { LiveKitPermissions, MeetPermissions, MeetRoomMemberRole, MeetUser } from '@openvidu-meet/typings'; +import { + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetUser +} from '@openvidu-meet/typings'; import { AsyncLocalStorage } from 'async_hooks'; import { injectable } from 'inversify'; import { RequestContext } from '../models/request-context.model.js'; @@ -66,58 +71,41 @@ export class RequestSessionService { /** * Gets the authenticated user from the current request context. */ - getUser(): MeetUser | undefined { + getAuthenticatedUser(): MeetUser | undefined { return this.getContext()?.user; } /** - * Sets the room member token information (role, permissions, and token claims) + * Sets the room member token metadata (room ID, base role, permissions) * in the current request context. * If called outside a request context, this operation is silently ignored. */ - setRoomMemberTokenInfo( - role: MeetRoomMemberRole, - meetPermissions: MeetPermissions, - livekitPermissions: LiveKitPermissions - ): void { + setRoomMemberTokenMetadata(metadata: MeetRoomMemberTokenMetadata): void { const context = this.getContext(); if (context) { - context.roomMember = { - role, - permissions: { - meet: meetPermissions, - livekit: livekitPermissions - } - }; + context.roomMember = metadata; } } /** - * Gets the room member role from the current request context. + * Gets the room ID to which the room member belongs from the current request context. */ - getRoomMemberRole(): MeetRoomMemberRole | undefined { - return this.getContext()?.roomMember?.role; + getRoomIdFromMember(): string | undefined { + return this.getContext()?.roomMember?.roomId; } /** - * Gets the room member Meet permissions from the current request context. + * Gets the room member base role from the current request context. */ - getRoomMemberMeetPermissions(): MeetPermissions | undefined { - return this.getContext()?.roomMember?.permissions.meet; + getRoomMemberBaseRole(): MeetRoomMemberRole | undefined { + return this.getContext()?.roomMember?.baseRole; } /** - * Gets the room member LiveKit permissions from the current request context. + * Gets the room member effective permissions from the current request context. */ - getRoomMemberLivekitPermissions(): LiveKitPermissions | undefined { - return this.getContext()?.roomMember?.permissions.livekit; - } - - /** - * Gets the room ID from the token claims in the current request context. - */ - getRoomIdFromToken(): string | undefined { - return this.getContext()?.roomMember?.permissions.livekit.room; + getRoomMemberPermissions(): MeetRoomMemberPermissions | undefined { + return this.getContext()?.roomMember?.effectivePermissions; } } diff --git a/meet-ce/backend/src/services/token.service.ts b/meet-ce/backend/src/services/token.service.ts index 7ed0536d..32532458 100644 --- a/meet-ce/backend/src/services/token.service.ts +++ b/meet-ce/backend/src/services/token.service.ts @@ -69,8 +69,8 @@ export class TokenService { } async verifyToken(token: string): Promise { - const verifyer = new TokenVerifier(MEET_ENV.LIVEKIT_API_KEY, MEET_ENV.LIVEKIT_API_SECRET); - return await verifyer.verify(token, 0); + const verifier = new TokenVerifier(MEET_ENV.LIVEKIT_API_KEY, MEET_ENV.LIVEKIT_API_SECRET); + return await verifier.verify(token, 0); } /** diff --git a/meet-ce/backend/src/services/user.service.ts b/meet-ce/backend/src/services/user.service.ts index b0c5cde8..782fe65e 100644 --- a/meet-ce/backend/src/services/user.service.ts +++ b/meet-ce/backend/src/services/user.service.ts @@ -1,6 +1,5 @@ import { MeetUser, MeetUserDTO, MeetUserRole } from '@openvidu-meet/typings'; import { inject, injectable } from 'inversify'; -import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_ENV } from '../environment.js'; import { PasswordHelper } from '../helpers/password.helper.js'; import { errorInvalidPassword, internalError } from '../models/error.model.js'; @@ -27,17 +26,18 @@ export class UserService { } const admin: MeetUser = { - username: MEET_ENV.INITIAL_ADMIN_USER, - passwordHash: await PasswordHelper.hashPassword(MEET_ENV.INITIAL_ADMIN_PASSWORD), - roles: [MeetUserRole.ADMIN, MeetUserRole.USER] + userId: MEET_ENV.INITIAL_ADMIN_USER, + name: 'Admin', + role: MeetUserRole.ADMIN, + passwordHash: await PasswordHelper.hashPassword(MEET_ENV.INITIAL_ADMIN_PASSWORD) }; await this.userRepository.create(admin); this.logger.info(`Admin user initialized with default credentials`); } - async authenticateUser(username: string, password: string): Promise { - const user = await this.getUser(username); + async authenticateUser(userId: string, password: string): Promise { + const user = await this.getUser(userId); if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) { return null; @@ -46,24 +46,13 @@ export class UserService { return user; } - async getUser(username: string): Promise { - return this.userRepository.findByUsername(username); + async getUser(userId: string): Promise { + return this.userRepository.findByUsername(userId); } - getAnonymousUser(): MeetUser { - return { - username: INTERNAL_CONFIG.ANONYMOUS_USER, - passwordHash: '', - roles: [MeetUserRole.USER] - }; - } - - getApiUser(): MeetUser { - return { - username: INTERNAL_CONFIG.API_USER, - passwordHash: '', - roles: [MeetUserRole.APP] - }; + async getUserAssociatedWithApiKey(): Promise { + // Return admin user for API key access + return this.userRepository.findByUsername(MEET_ENV.INITIAL_ADMIN_USER); } async changePassword(username: string, currentPassword: string, newPassword: string) {