backend: enhance API key handling and validation, including error handling for missing keys

This commit is contained in:
juancarmore 2025-06-19 12:19:54 +02:00
parent bd512f782c
commit 5dcde7a153
6 changed files with 64 additions and 15 deletions

View File

@ -20,7 +20,7 @@ export const login = async (req: Request, res: Response) => {
const { username, password } = req.body as { username: string; password: string };
const authService = container.get(AuthService);
const user = await authService.authenticate(username, password);
const user = await authService.authenticateUser(username, password);
if (!user) {
logger.warn('Login failed');

View File

@ -7,7 +7,7 @@ if (process.env.MEET_CONFIG_DIR) {
envPath = process.env.MEET_CONFIG_DIR;
} else if (process.env.NODE_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';
} else {
envPath = undefined;
@ -22,7 +22,7 @@ export const {
MEET_NAME_ID = 'openviduMeet',
// Authentication configuration
MEET_API_KEY = 'meet-api-key',
MEET_API_KEY = '',
MEET_ADMIN_USER = 'admin',
MEET_ADMIN_SECRET = 'admin',

View File

@ -5,7 +5,6 @@ import { ClaimGrants } from 'livekit-server-sdk';
import ms from 'ms';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MEET_API_KEY } from '../environment.js';
import {
errorInsufficientPermissions,
errorInvalidApiKey,
@ -16,7 +15,7 @@ import {
OpenViduMeetError,
rejectRequestFromMeetError
} 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.
@ -130,8 +129,15 @@ export const apiKeyValidator = async (req: Request) => {
throw errorWithControl(errorUnauthorized(), false);
}
if (apiKey !== MEET_API_KEY) {
throw errorWithControl(errorInvalidApiKey(), true);
try {
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);

View File

@ -92,6 +92,14 @@ export const errorInvalidApiKey = (): OpenViduMeetError => {
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
export const errorRecordingDisabled = (roomId: string): OpenViduMeetError => {
@ -135,7 +143,11 @@ export const errorRoomHasNoParticipants = (roomId: 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 => {

View File

@ -1,6 +1,8 @@
import { User } from '@typings-ce';
import { inject, injectable } from 'inversify';
import { MEET_API_KEY } from '../environment.js';
import { PasswordHelper } from '../helpers/index.js';
import { errorApiKeyNotConfigured } from '../models/error.model.js';
import { MeetStorageService, UserService } from './index.js';
@injectable()
@ -10,7 +12,7 @@ export class AuthService {
@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);
if (!user || !(await PasswordHelper.verifyPassword(password, user.passwordHash))) {
@ -35,4 +37,15 @@ export class AuthService {
await this.storageService.deleteApiKeys();
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;
}
}

View File

@ -9,13 +9,14 @@ import {
import crypto from 'crypto';
import { inject, injectable } from 'inversify';
import { MEET_API_KEY } from '../environment.js';
import { LoggerService, MeetStorageService } from './index.js';
import { AuthService, LoggerService, MeetStorageService } from './index.js';
@injectable()
export class OpenViduWebhookService {
constructor(
@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
};
const signature = this.generateWebhookSignature(creationDate, data);
this.logger.info(`Sending webhook event ${data.event}`);
try {
const signature = await this.generateWebhookSignature(creationDate, data);
await this.fetchWithRetry(webhookPreferences.url!, {
method: 'POST',
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
.createHmac('sha256', MEET_API_KEY)
.createHmac('sha256', apiKey)
.update(`${timestamp}.${JSON.stringify(payload)}`)
.digest('hex');
}
@ -177,4 +179,20 @@ export class OpenViduWebhookService {
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;
}
}