backend: refactor auth middlewares and data stored in request context
This commit is contained in:
parent
53ac60d37a
commit
5d874f57f5
@ -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',
|
||||
|
||||
@ -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<boolean> {
|
||||
@ -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<boolean> {
|
||||
@ -219,14 +222,15 @@ export const allowAnonymous: AuthValidator = {
|
||||
async validate(req: Request): Promise<void> {
|
||||
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<MeetUser> => {
|
||||
const userService = container.get(UserService);
|
||||
// Return the authenticated user if available, otherwise return null
|
||||
const getAuthenticatedUserOrAnonymous = async (req: Request): Promise<MeetUser | null> => {
|
||||
let user: MeetUser | null = null;
|
||||
|
||||
// Check if there is a user already authenticated
|
||||
@ -236,18 +240,16 @@ const getAuthenticatedUserOrAnonymous = async (req: Request): Promise<MeetUser>
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,8 +69,8 @@ export class TokenService {
|
||||
}
|
||||
|
||||
async verifyToken(token: string): Promise<ClaimGrants> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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<MeetUser | null> {
|
||||
const user = await this.getUser(username);
|
||||
async authenticateUser(userId: string, password: string): Promise<MeetUser | null> {
|
||||
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<MeetUser | null> {
|
||||
return this.userRepository.findByUsername(username);
|
||||
async getUser(userId: string): Promise<MeetUser | null> {
|
||||
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<MeetUser | null> {
|
||||
// Return admin user for API key access
|
||||
return this.userRepository.findByUsername(MEET_ENV.INITIAL_ADMIN_USER);
|
||||
}
|
||||
|
||||
async changePassword(username: string, currentPassword: string, newPassword: string) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user