backend: enhance API key handling and validation, including error handling for missing keys
This commit is contained in:
parent
bd512f782c
commit
5dcde7a153
@ -20,7 +20,7 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
const { username, password } = req.body as { username: string; password: string };
|
const { username, password } = req.body as { username: string; password: string };
|
||||||
|
|
||||||
const authService = container.get(AuthService);
|
const authService = container.get(AuthService);
|
||||||
const user = await authService.authenticate(username, password);
|
const user = await authService.authenticateUser(username, password);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn('Login failed');
|
logger.warn('Login failed');
|
||||||
|
|||||||
@ -7,7 +7,7 @@ if (process.env.MEET_CONFIG_DIR) {
|
|||||||
envPath = process.env.MEET_CONFIG_DIR;
|
envPath = process.env.MEET_CONFIG_DIR;
|
||||||
} else if (process.env.NODE_ENV === 'development') {
|
} else if (process.env.NODE_ENV === 'development') {
|
||||||
envPath = '.env.development';
|
envPath = '.env.development';
|
||||||
} else if (process.env.NODE_ENV === 'test') {
|
} else if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'ci') {
|
||||||
envPath = '.env.test';
|
envPath = '.env.test';
|
||||||
} else {
|
} else {
|
||||||
envPath = undefined;
|
envPath = undefined;
|
||||||
@ -22,7 +22,7 @@ export const {
|
|||||||
MEET_NAME_ID = 'openviduMeet',
|
MEET_NAME_ID = 'openviduMeet',
|
||||||
|
|
||||||
// Authentication configuration
|
// Authentication configuration
|
||||||
MEET_API_KEY = 'meet-api-key',
|
MEET_API_KEY = '',
|
||||||
MEET_ADMIN_USER = 'admin',
|
MEET_ADMIN_USER = 'admin',
|
||||||
MEET_ADMIN_SECRET = 'admin',
|
MEET_ADMIN_SECRET = 'admin',
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { ClaimGrants } from 'livekit-server-sdk';
|
|||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { container } from '../config/index.js';
|
import { container } from '../config/index.js';
|
||||||
import INTERNAL_CONFIG from '../config/internal-config.js';
|
import INTERNAL_CONFIG from '../config/internal-config.js';
|
||||||
import { MEET_API_KEY } from '../environment.js';
|
|
||||||
import {
|
import {
|
||||||
errorInsufficientPermissions,
|
errorInsufficientPermissions,
|
||||||
errorInvalidApiKey,
|
errorInvalidApiKey,
|
||||||
@ -16,7 +15,7 @@ import {
|
|||||||
OpenViduMeetError,
|
OpenViduMeetError,
|
||||||
rejectRequestFromMeetError
|
rejectRequestFromMeetError
|
||||||
} from '../models/index.js';
|
} from '../models/index.js';
|
||||||
import { LoggerService, TokenService, UserService } from '../services/index.js';
|
import { AuthService, LoggerService, TokenService, UserService } from '../services/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware allows to chain multiple validators to check if the request is authorized.
|
* This middleware allows to chain multiple validators to check if the request is authorized.
|
||||||
@ -130,8 +129,15 @@ export const apiKeyValidator = async (req: Request) => {
|
|||||||
throw errorWithControl(errorUnauthorized(), false);
|
throw errorWithControl(errorUnauthorized(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey !== MEET_API_KEY) {
|
try {
|
||||||
throw errorWithControl(errorInvalidApiKey(), true);
|
const authService = container.get(AuthService);
|
||||||
|
const isValidApiKey = await authService.validateApiKey(apiKey as string);
|
||||||
|
|
||||||
|
if (!isValidApiKey) {
|
||||||
|
throw errorInvalidApiKey();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw errorWithControl(error as OpenViduMeetError, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userService = container.get(UserService);
|
const userService = container.get(UserService);
|
||||||
|
|||||||
@ -92,6 +92,14 @@ export const errorInvalidApiKey = (): OpenViduMeetError => {
|
|||||||
return new OpenViduMeetError('Authentication Error', 'Invalid API key', 401);
|
return new OpenViduMeetError('Authentication Error', 'Invalid API key', 401);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const errorApiKeyNotConfigured = (): OpenViduMeetError => {
|
||||||
|
return new OpenViduMeetError(
|
||||||
|
'Authentication Error',
|
||||||
|
'There are no API keys configured yet. Please, create one to access the API',
|
||||||
|
401
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Recording errors
|
// Recording errors
|
||||||
|
|
||||||
export const errorRecordingDisabled = (roomId: string): OpenViduMeetError => {
|
export const errorRecordingDisabled = (roomId: string): OpenViduMeetError => {
|
||||||
@ -135,7 +143,11 @@ export const errorRoomHasNoParticipants = (roomId: string): OpenViduMeetError =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const errorInvalidRecordingSecret = (recordingId: string, secret: string): OpenViduMeetError => {
|
export const errorInvalidRecordingSecret = (recordingId: string, secret: string): OpenViduMeetError => {
|
||||||
return new OpenViduMeetError('Recording Error', `Secret '${secret}' is not recognized for recording '${recordingId}'`, 400);
|
return new OpenViduMeetError(
|
||||||
|
'Recording Error',
|
||||||
|
`Secret '${secret}' is not recognized for recording '${recordingId}'`,
|
||||||
|
400
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
|
const isMatchingError = (error: OpenViduMeetError, originalError: OpenViduMeetError): boolean => {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { User } from '@typings-ce';
|
import { User } from '@typings-ce';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
|
import { MEET_API_KEY } from '../environment.js';
|
||||||
import { PasswordHelper } from '../helpers/index.js';
|
import { PasswordHelper } from '../helpers/index.js';
|
||||||
|
import { errorApiKeyNotConfigured } from '../models/error.model.js';
|
||||||
import { MeetStorageService, UserService } from './index.js';
|
import { MeetStorageService, UserService } from './index.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -10,7 +12,7 @@ export class AuthService {
|
|||||||
@inject(MeetStorageService) protected storageService: MeetStorageService
|
@inject(MeetStorageService) protected storageService: MeetStorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async authenticate(username: string, password: string): Promise<User | null> {
|
async authenticateUser(username: string, password: string): Promise<User | null> {
|
||||||
const user = await this.userService.getUser(username);
|
const user = await this.userService.getUser(username);
|
||||||
|
|
||||||
if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) {
|
if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) {
|
||||||
@ -35,4 +37,15 @@ export class AuthService {
|
|||||||
await this.storageService.deleteApiKeys();
|
await this.storageService.deleteApiKeys();
|
||||||
return { message: 'API keys deleted successfully' };
|
return { message: 'API keys deleted successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateApiKey(apiKey: string): Promise<boolean> {
|
||||||
|
const storedApiKeys = await this.getApiKeys();
|
||||||
|
|
||||||
|
if (storedApiKeys.length === 0 && !MEET_API_KEY) {
|
||||||
|
throw errorApiKeyNotConfigured();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the provided API key matches any stored API key or the MEET_API_KEY
|
||||||
|
return storedApiKeys.some((key) => key.key === apiKey) || apiKey === MEET_API_KEY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,13 +9,14 @@ import {
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { MEET_API_KEY } from '../environment.js';
|
import { MEET_API_KEY } from '../environment.js';
|
||||||
import { LoggerService, MeetStorageService } from './index.js';
|
import { AuthService, LoggerService, MeetStorageService } from './index.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenViduWebhookService {
|
export class OpenViduWebhookService {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(LoggerService) protected logger: LoggerService,
|
@inject(LoggerService) protected logger: LoggerService,
|
||||||
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
|
@inject(MeetStorageService) protected globalPrefService: MeetStorageService,
|
||||||
|
@inject(AuthService) protected authService: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,11 +123,11 @@ export class OpenViduWebhookService {
|
|||||||
data: payload
|
data: payload
|
||||||
};
|
};
|
||||||
|
|
||||||
const signature = this.generateWebhookSignature(creationDate, data);
|
|
||||||
|
|
||||||
this.logger.info(`Sending webhook event ${data.event}`);
|
this.logger.info(`Sending webhook event ${data.event}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const signature = await this.generateWebhookSignature(creationDate, data);
|
||||||
|
|
||||||
await this.fetchWithRetry(webhookPreferences.url!, {
|
await this.fetchWithRetry(webhookPreferences.url!, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -142,9 +143,10 @@ export class OpenViduWebhookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected generateWebhookSignature(timestamp: number, payload: object): string {
|
protected async generateWebhookSignature(timestamp: number, payload: object): Promise<string> {
|
||||||
|
const apiKey = await this.getApiKey();
|
||||||
return crypto
|
return crypto
|
||||||
.createHmac('sha256', MEET_API_KEY)
|
.createHmac('sha256', apiKey)
|
||||||
.update(`${timestamp}.${JSON.stringify(payload)}`)
|
.update(`${timestamp}.${JSON.stringify(payload)}`)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
}
|
}
|
||||||
@ -177,4 +179,20 @@ export class OpenViduWebhookService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async getApiKey(): Promise<string> {
|
||||||
|
const apiKeys = await this.authService.getApiKeys();
|
||||||
|
|
||||||
|
if (apiKeys.length === 0) {
|
||||||
|
// If no API keys are configured, check if the MEET_API_KEY environment variable is set
|
||||||
|
if (MEET_API_KEY) {
|
||||||
|
return MEET_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('There are no API keys configured yet. Please, create one to use webhooks.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first API key
|
||||||
|
return apiKeys[0].key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user