From d1af9637a69391fb93009e5d685883b2efece260 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 21 Mar 2025 00:56:19 +0100 Subject: [PATCH] backend: Remove BasicAuth and refactor authentication middlewares to be able to use multiple options --- backend/package-lock.json | 28 ---- backend/package.json | 1 - backend/src/middlewares/auth.middleware.ts | 170 ++++++++++----------- backend/src/models/error.model.ts | 38 +++-- 4 files changed, 108 insertions(+), 129 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 26639fc..0040494 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,6 @@ "cron": "^4.1.0", "dotenv": "16.4.7", "express": "4.21.2", - "express-basic-auth": "1.2.1", "express-openapi-validator": "^5.4.2", "express-rate-limit": "^7.5.0", "inversify": "^6.2.1", @@ -4713,24 +4712,6 @@ ], "license": "MIT" }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -6523,15 +6504,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-basic-auth": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", - "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", - "license": "MIT", - "dependencies": { - "basic-auth": "^2.0.1" - } - }, "node_modules/express-openapi-validator": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.4.2.tgz", diff --git a/backend/package.json b/backend/package.json index 3f1098e..68cbea7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,7 +47,6 @@ "cron": "^4.1.0", "dotenv": "16.4.7", "express": "4.21.2", - "express-basic-auth": "1.2.1", "express-openapi-validator": "^5.4.2", "express-rate-limit": "^7.5.0", "inversify": "^6.2.1", diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index 38b429f..03614c4 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -1,122 +1,116 @@ -import { NextFunction, Request, Response } from 'express'; -import basicAuth from 'express-basic-auth'; -import { TokenService } from '../services/token.service.js'; +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { TokenService, UserService } from '../services/index.js'; import { ACCESS_TOKEN_COOKIE_NAME, - MEET_ADMIN_SECRET, - MEET_ADMIN_USER, MEET_API_KEY, MEET_PRIVATE_ACCESS, - MEET_SECRET, - MEET_USER, PARTICIPANT_TOKEN_COOKIE_NAME } from '../environment.js'; import { container } from '../config/dependency-injector.config.js'; +import { ClaimGrants } from 'livekit-server-sdk'; +import { Role } from '@typings-ce'; +import { + errorUnauthorized, + errorInvalidToken, + errorInvalidTokenSubject, + errorInsufficientPermissions, + errorInvalidApiKey, + OpenViduMeetError +} from '../models/index.js'; -// Configure token validation middleware for admin access -export const withAdminValidToken = async (req: Request, res: Response, next: NextFunction) => { - const token = req.cookies[ACCESS_TOKEN_COOKIE_NAME]; +export const withAuth = (...validators: ((req: Request) => Promise)[]): RequestHandler => { + return async (req: Request, res: Response, next: NextFunction) => { + let lastError: OpenViduMeetError | null = null; - if (!token) { - return res.status(401).json({ message: 'Unauthorized' }); - } - - const tokenService = container.get(TokenService); - - try { - const payload = await tokenService.verifyToken(token); - - if (payload.sub !== MEET_ADMIN_USER) { - return res.status(403).json({ message: 'Invalid token subject' }); + for (const middleware of validators) { + try { + await middleware(req); + // If any middleware granted access, it is not necessary to continue checking the rest + return next(); + } catch (error) { + // If no middleware granted access, return unauthorized + if (error instanceof OpenViduMeetError) { + lastError = error; + } + } } - } catch (error) { - return res.status(401).json({ message: 'Invalid token' }); - } - next(); + if (lastError) { + return res.status(lastError.statusCode).json({ message: lastError.message }); + } + + return res.status(500).json({ message: 'Internal server error' }); + }; }; -export const withParticipantValidToken = async (req: Request, res: Response, next: NextFunction) => { +// Configure token validatior for role-based access +export const tokenAndRoleValidator = (role: Role) => { + return async (req: Request) => { + // Skip token validation if role is USER and access is public + if (role == Role.USER && MEET_PRIVATE_ACCESS === 'false') { + return; + } + + const token = req.cookies[ACCESS_TOKEN_COOKIE_NAME]; + + if (!token) { + throw errorUnauthorized(); + } + + const tokenService = container.get(TokenService); + let payload: ClaimGrants; + + try { + payload = await tokenService.verifyToken(token); + } catch (error) { + throw errorInvalidToken(); + } + + const username = payload.sub; + const userService = container.get(UserService); + const user = username ? userService.getUser(username) : null; + + if (!user) { + throw errorInvalidTokenSubject(); + } + + if (user.role !== role) { + throw errorInsufficientPermissions(); + } + + req.session = req.session || {}; + req.session.user = user; + }; +}; + +// Configure token validatior for participant access +export const participantTokenValidator = async (req: Request) => { const token = req.cookies[PARTICIPANT_TOKEN_COOKIE_NAME]; if (!token) { - return res.status(401).json({ message: 'Unauthorized' }); + throw errorUnauthorized(); } const tokenService = container.get(TokenService); try { const payload = await tokenService.verifyToken(token); - - // Parse metadata if it exists and add payload to request body for further processing - if (payload.metadata) { - payload.metadata = JSON.parse(payload.metadata); - } - - req.body.payload = payload; + req.session = req.session || {}; + req.session.tokenClaims = payload; } catch (error) { - return res.status(401).json({ message: 'Invalid token' }); + throw errorInvalidToken(); } - - next(); }; -export const withValidApiKey = async (req: Request, res: Response, next: NextFunction) => { +// Configure API key validatior +export const apiKeyValidator = async (req: Request) => { const apiKey = req.headers['x-api-key']; if (!apiKey) { - return res.status(401).json({ message: 'Unauthorized' }); + throw errorUnauthorized(); } if (apiKey !== MEET_API_KEY) { - return res.status(401).json({ message: 'Invalid API key' }); - } - - next(); -}; - -// Configure basic auth middleware for user and admin access -export const withAdminAndUserBasicAuth = (req: Request, res: Response, next: NextFunction) => { - if (MEET_PRIVATE_ACCESS === 'true') { - // Configure basic auth middleware if access is private - const basicAuthMiddleware = basicAuth({ - users: { - [MEET_USER]: MEET_SECRET, - [MEET_ADMIN_USER]: MEET_ADMIN_SECRET - }, - challenge: true, - unauthorizedResponse: () => 'Unauthorized' - }); - return basicAuthMiddleware(req, res, next); - } else { - // Skip basic auth if access is public - next(); - } -}; - -// Configure basic auth middleware for admin access -export const withAdminBasicAuth = basicAuth({ - users: { - [MEET_ADMIN_USER]: MEET_ADMIN_SECRET - }, - challenge: true, - unauthorizedResponse: () => 'Unauthorized' -}); - -// Configure basic auth middleware for user access -export const withUserBasicAuth = (req: Request, res: Response, next: NextFunction) => { - if (MEET_PRIVATE_ACCESS === 'true') { - // Configure basic auth middleware if access is private - const basicAuthMiddleware = basicAuth({ - users: { - [MEET_USER]: MEET_SECRET - }, - challenge: true, - unauthorizedResponse: () => 'Unauthorized' - }); - return basicAuthMiddleware(req, res, next); - } else { - // Skip basic auth if access is public - next(); + throw errorInvalidApiKey(); } }; diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 10fbd22..811edd6 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -1,4 +1,4 @@ -type StatusError = 400 | 404 | 406 | 409 | 422 | 500 | 503; +type StatusError = 400 | 401 | 403 | 404 | 406 | 409 | 422 | 500 | 503; export class OpenViduMeetError extends Error { name: string; statusCode: StatusError; @@ -17,7 +17,7 @@ export const errorLivekitIsNotAvailable = (): OpenViduMeetError => { export const errorS3NotAvailable = (error: any): OpenViduMeetError => { return new OpenViduMeetError('S3 Error', `S3 is not available ${error}`, 503); -} +}; export const internalError = (error: any): OpenViduMeetError => { return new OpenViduMeetError('Unexpected error', `Something went wrong ${error}`, 500); @@ -31,6 +31,28 @@ export const errorUnprocessableParams = (error: string): OpenViduMeetError => { return new OpenViduMeetError('Wrong request', `Some parameters are not valid. ${error}`, 422); }; +// Auth errors + +export const errorUnauthorized = (): OpenViduMeetError => { + return new OpenViduMeetError('Authentication error', 'Unauthorized', 401); +}; + +export const errorInvalidToken = (): OpenViduMeetError => { + return new OpenViduMeetError('Authentication error', 'Invalid token', 401); +}; + +export const errorInvalidTokenSubject = (): OpenViduMeetError => { + return new OpenViduMeetError('Authentication error', 'Invalid token subject', 403); +}; + +export const errorInsufficientPermissions = (): OpenViduMeetError => { + return new OpenViduMeetError('Authentication error', 'You do not have permission to access this resource', 403); +}; + +export const errorInvalidApiKey = (): OpenViduMeetError => { + return new OpenViduMeetError('Authentication error', 'Invalid API key', 401); +}; + // Recording errors export const errorRecordingNotFound = (recordingId: string): OpenViduMeetError => { @@ -69,17 +91,9 @@ export const errorParticipantUnauthorized = (roomName: string): OpenViduMeetErro }; export const errorParticipantNotFound = (participantName: string, roomName: string): OpenViduMeetError => { - return new OpenViduMeetError( - 'Participant Error', - `'${participantName}' not found in room '${roomName}'`, - 404 - ); + return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomName}'`, 404); }; export const errorParticipantAlreadyExists = (participantName: string, roomName: string): OpenViduMeetError => { - return new OpenViduMeetError( - 'Room Error', - `'${participantName}' already exists in room in ${roomName}`, - 409 - ); + return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomName}`, 409); };