backend: Remove BasicAuth and refactor authentication middlewares to be able to use multiple options
This commit is contained in:
parent
a9274005ef
commit
d1af9637a6
28
backend/package-lock.json
generated
28
backend/package-lock.json
generated
@ -16,7 +16,6 @@
|
|||||||
"cron": "^4.1.0",
|
"cron": "^4.1.0",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-basic-auth": "1.2.1",
|
|
||||||
"express-openapi-validator": "^5.4.2",
|
"express-openapi-validator": "^5.4.2",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"inversify": "^6.2.1",
|
"inversify": "^6.2.1",
|
||||||
@ -4713,24 +4712,6 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/basic-ftp": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||||
@ -6523,15 +6504,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/express-openapi-validator": {
|
||||||
"version": "5.4.2",
|
"version": "5.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.4.2.tgz",
|
||||||
|
|||||||
@ -47,7 +47,6 @@
|
|||||||
"cron": "^4.1.0",
|
"cron": "^4.1.0",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-basic-auth": "1.2.1",
|
|
||||||
"express-openapi-validator": "^5.4.2",
|
"express-openapi-validator": "^5.4.2",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"inversify": "^6.2.1",
|
"inversify": "^6.2.1",
|
||||||
|
|||||||
@ -1,122 +1,116 @@
|
|||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||||
import basicAuth from 'express-basic-auth';
|
import { TokenService, UserService } from '../services/index.js';
|
||||||
import { TokenService } from '../services/token.service.js';
|
|
||||||
import {
|
import {
|
||||||
ACCESS_TOKEN_COOKIE_NAME,
|
ACCESS_TOKEN_COOKIE_NAME,
|
||||||
MEET_ADMIN_SECRET,
|
|
||||||
MEET_ADMIN_USER,
|
|
||||||
MEET_API_KEY,
|
MEET_API_KEY,
|
||||||
MEET_PRIVATE_ACCESS,
|
MEET_PRIVATE_ACCESS,
|
||||||
MEET_SECRET,
|
|
||||||
MEET_USER,
|
|
||||||
PARTICIPANT_TOKEN_COOKIE_NAME
|
PARTICIPANT_TOKEN_COOKIE_NAME
|
||||||
} from '../environment.js';
|
} from '../environment.js';
|
||||||
import { container } from '../config/dependency-injector.config.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 withAuth = (...validators: ((req: Request) => Promise<void>)[]): RequestHandler => {
|
||||||
export const withAdminValidToken = async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const token = req.cookies[ACCESS_TOKEN_COOKIE_NAME];
|
let lastError: OpenViduMeetError | null = null;
|
||||||
|
|
||||||
if (!token) {
|
for (const middleware of validators) {
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
try {
|
||||||
}
|
await middleware(req);
|
||||||
|
// If any middleware granted access, it is not necessary to continue checking the rest
|
||||||
const tokenService = container.get(TokenService);
|
return next();
|
||||||
|
} catch (error) {
|
||||||
try {
|
// If no middleware granted access, return unauthorized
|
||||||
const payload = await tokenService.verifyToken(token);
|
if (error instanceof OpenViduMeetError) {
|
||||||
|
lastError = error;
|
||||||
if (payload.sub !== MEET_ADMIN_USER) {
|
}
|
||||||
return res.status(403).json({ message: 'Invalid token subject' });
|
}
|
||||||
}
|
}
|
||||||
} 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];
|
const token = req.cookies[PARTICIPANT_TOKEN_COOKIE_NAME];
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
throw errorUnauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenService = container.get(TokenService);
|
const tokenService = container.get(TokenService);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await tokenService.verifyToken(token);
|
const payload = await tokenService.verifyToken(token);
|
||||||
|
req.session = req.session || {};
|
||||||
// Parse metadata if it exists and add payload to request body for further processing
|
req.session.tokenClaims = payload;
|
||||||
if (payload.metadata) {
|
|
||||||
payload.metadata = JSON.parse(payload.metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body.payload = payload;
|
|
||||||
} catch (error) {
|
} 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'];
|
const apiKey = req.headers['x-api-key'];
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
throw errorUnauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey !== MEET_API_KEY) {
|
if (apiKey !== MEET_API_KEY) {
|
||||||
return res.status(401).json({ message: 'Invalid API key' });
|
throw errorInvalidApiKey();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 {
|
export class OpenViduMeetError extends Error {
|
||||||
name: string;
|
name: string;
|
||||||
statusCode: StatusError;
|
statusCode: StatusError;
|
||||||
@ -17,7 +17,7 @@ export const errorLivekitIsNotAvailable = (): OpenViduMeetError => {
|
|||||||
|
|
||||||
export const errorS3NotAvailable = (error: any): OpenViduMeetError => {
|
export const errorS3NotAvailable = (error: any): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('S3 Error', `S3 is not available ${error}`, 503);
|
return new OpenViduMeetError('S3 Error', `S3 is not available ${error}`, 503);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const internalError = (error: any): OpenViduMeetError => {
|
export const internalError = (error: any): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Unexpected error', `Something went wrong ${error}`, 500);
|
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);
|
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
|
// Recording errors
|
||||||
|
|
||||||
export const errorRecordingNotFound = (recordingId: string): OpenViduMeetError => {
|
export const errorRecordingNotFound = (recordingId: string): OpenViduMeetError => {
|
||||||
@ -69,17 +91,9 @@ export const errorParticipantUnauthorized = (roomName: string): OpenViduMeetErro
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const errorParticipantNotFound = (participantName: string, roomName: string): OpenViduMeetError => {
|
export const errorParticipantNotFound = (participantName: string, roomName: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError(
|
return new OpenViduMeetError('Participant Error', `'${participantName}' not found in room '${roomName}'`, 404);
|
||||||
'Participant Error',
|
|
||||||
`'${participantName}' not found in room '${roomName}'`,
|
|
||||||
404
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorParticipantAlreadyExists = (participantName: string, roomName: string): OpenViduMeetError => {
|
export const errorParticipantAlreadyExists = (participantName: string, roomName: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError(
|
return new OpenViduMeetError('Room Error', `'${participantName}' already exists in room in ${roomName}`, 409);
|
||||||
'Room Error',
|
|
||||||
`'${participantName}' already exists in room in ${roomName}`,
|
|
||||||
409
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user