From f732ddbe67d95905f807a0117d34fc1cf526adc5 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Wed, 18 Jun 2025 09:19:28 +0200 Subject: [PATCH] WIP: Added api-keys endpoints, openapi docs and tests --- .../internal/create-api-key.yaml | 19 +++++++ .../openapi/openvidu-meet-internal-api.yaml | 2 + backend/openapi/paths/internal/auth.yaml | 53 +++++++++++++++++++ backend/src/config/internal-config.ts | 1 + backend/src/controllers/auth.controller.ts | 42 +++++++++++++++ backend/src/helpers/password.helper.ts | 6 +++ backend/src/models/redis.model.ts | 3 +- backend/src/routes/auth.routes.ts | 8 ++- backend/src/services/auth.service.ts | 23 +++++++- .../providers/s3/s3-storage-key.builder.ts | 4 ++ .../src/services/storage/storage.interface.ts | 6 ++- .../src/services/storage/storage.service.ts | 37 +++++++++++-- 12 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 backend/openapi/components/requestBodies/internal/create-api-key.yaml diff --git a/backend/openapi/components/requestBodies/internal/create-api-key.yaml b/backend/openapi/components/requestBodies/internal/create-api-key.yaml new file mode 100644 index 0000000..9ab44aa --- /dev/null +++ b/backend/openapi/components/requestBodies/internal/create-api-key.yaml @@ -0,0 +1,19 @@ +description: Create a new API key. +required: true +content: + application/json: + schema: + type: object + properties: + apiKey: + type: string + description: The API key to be created. + example: "ovmeet-1234567890abcdef1234567890abcdef" + creationDate: + type: string + format: date-time + description: The date and time when the API key was created. + example: "2023-10-01T12:00:00Z" + required: + - apiKey + additionalProperties: false \ No newline at end of file diff --git a/backend/openapi/openvidu-meet-internal-api.yaml b/backend/openapi/openvidu-meet-internal-api.yaml index 8146c7c..f3ed3e7 100644 --- a/backend/openapi/openvidu-meet-internal-api.yaml +++ b/backend/openapi/openvidu-meet-internal-api.yaml @@ -14,6 +14,8 @@ paths: $ref: './paths/internal/auth.yaml#/~1auth~1logout' /auth/refresh: $ref: './paths/internal/auth.yaml#/~1auth~1refresh' + /auth/api-keys: + $ref: './paths/internal/auth.yaml#/~1auth~1api-keys' /users/profile: $ref: './paths/internal/users.yaml#/~1users~1profile' /users/change-password: diff --git a/backend/openapi/paths/internal/auth.yaml b/backend/openapi/paths/internal/auth.yaml index bf7571c..6140feb 100644 --- a/backend/openapi/paths/internal/auth.yaml +++ b/backend/openapi/paths/internal/auth.yaml @@ -63,3 +63,56 @@ $ref: '../../components/responses/internal/error-invalid-refresh-token.yaml' '500': $ref: '../../components/responses/internal-server-error.yaml' + +/auth/api-keys: + post: + operationId: createApiKey + summary: Create an API key + description: > + Creates a new API key. Only one API key can be created in the system. + If an API key already exists, it will be replaced with the new one. + The API key is returned in the response. + tags: + - Internal API - Authentication + security: + - accessTokenCookie: [] + requestBody: + $ref: '../../components/requestBodies/internal/create-api-key.yaml' + responses: + '201': + $ref: '../../components/responses/internal/success-create-api-key.yaml' + '400': + $ref: '../../components/responses/internal/error-invalid-request.yaml' + '500': + $ref: '../../components/responses/internal-server-error.yaml' + get: + operationId: getApiKeys + summary: Get the API keys + description: > + Retrieves the existing API keys. + tags: + - Internal API - Authentication + security: + - accessTokenCookie: [] + responses: + '200': + $ref: '../../components/responses/internal/success-get-api-key.yaml' + '500': + $ref: '../../components/responses/internal-server-error.yaml' + delete: + operationId: deleteApiKeys + summary: Delete the API keys + description: > + Deletes the existing API keys. + tags: + - Internal API - Authentication + security: + - accessTokenCookie: [] + responses: + '204': + description: Successfully deleted the API key + '500': + $ref: '../../components/responses/internal-server-error.yaml' + + + diff --git a/backend/src/config/internal-config.ts b/backend/src/config/internal-config.ts index e50e424..3dea0ca 100644 --- a/backend/src/config/internal-config.ts +++ b/backend/src/config/internal-config.ts @@ -25,6 +25,7 @@ const INTERNAL_CONFIG = { S3_ROOMS_PREFIX: 'rooms', S3_RECORDINGS_PREFIX: 'recordings', S3_USERS_PREFIX: 'users', + S3_API_KEYS_PREFIX: 'api_keys', // Garbage collection and recording lock intervals ROOM_GC_INTERVAL: '1h' as StringValue, // e.g. garbage collector interval for rooms diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 8a5710c..028aa17 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -102,3 +102,45 @@ export const refreshToken = async (req: Request, res: Response) => { handleError(res, error, 'refreshing token'); } }; + +export const createApiKey = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + logger.verbose('Create API key request received'); + + const authService = container.get(AuthService); + + try { + const apiKey = await authService.createApiKey(); + return res.status(201).json({ apiKey }); + } catch (error) { + handleError(res, error, 'creating API key'); + } +}; + +export const deleteApiKeys = async (req: Request, res: Response) => { + const logger = container.get(LoggerService); + logger.verbose('Delete API keys request received'); + + const authService = container.get(AuthService); + + try { + await authService.deleteApiKeys(); + return res.status(202).send(); + } catch (error) { + handleError(res, error, 'deleting API keys'); + } +}; + +export const getApiKeys = async (_req: Request, res: Response) => { + const logger = container.get(LoggerService); + logger.verbose('Get API keys request received'); + + const authService = container.get(AuthService); + + try { + const apiKeys = await authService.getApiKeys(); + return res.status(200).json({ apiKeys }); + } catch (error) { + handleError(res, error, 'getting API keys'); + } +}; diff --git a/backend/src/helpers/password.helper.ts b/backend/src/helpers/password.helper.ts index 185e85f..a1b8c9f 100644 --- a/backend/src/helpers/password.helper.ts +++ b/backend/src/helpers/password.helper.ts @@ -1,4 +1,5 @@ import bcrypt from 'bcrypt'; +import { uid } from 'uid/secure'; const SALT_ROUNDS = 10; @@ -10,4 +11,9 @@ export class PasswordHelper { static async verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } + + // Generate a secure API key using uid with a length of 32 characters + static generateApiKey(): { key: string; creationDate: string } { + return { key: `ovmeet-${uid(32)}`, creationDate: new Date().toISOString() }; + } } diff --git a/backend/src/models/redis.model.ts b/backend/src/models/redis.model.ts index a745a4e..6327a98 100644 --- a/backend/src/models/redis.model.ts +++ b/backend/src/models/redis.model.ts @@ -8,7 +8,8 @@ export const enum RedisKeyName { RECORDING = `${RedisKeyPrefix.BASE}recording:`, RECORDING_SECRETS = `${RedisKeyPrefix.BASE}recording_secrets:`, ARCHIVED_ROOM = `${RedisKeyPrefix.BASE}archived_room:`, - USER = `${RedisKeyPrefix.BASE}user:` + USER = `${RedisKeyPrefix.BASE}user:`, + API_KEYS = `${RedisKeyPrefix.BASE}api_keys:`, } export const enum RedisLockPrefix { diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index b093f30..b8996be 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -1,7 +1,8 @@ import bodyParser from 'body-parser'; import { Router } from 'express'; import * as authCtrl from '../controllers/auth.controller.js'; -import { validateLoginRequest, withLoginLimiter } from '../middlewares/index.js'; +import { tokenAndRoleValidator, validateLoginRequest, withAuth, withLoginLimiter } from '../middlewares/index.js'; +import { UserRole } from '@typings-ce'; export const authRouter = Router(); authRouter.use(bodyParser.urlencoded({ extended: true })); @@ -11,3 +12,8 @@ authRouter.use(bodyParser.json()); authRouter.post('/login', validateLoginRequest, withLoginLimiter, authCtrl.login); authRouter.post('/logout', authCtrl.logout); authRouter.post('/refresh', authCtrl.refreshToken); + +// API Key Routes +authRouter.post('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.createApiKey); +authRouter.get('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.getApiKeys); +authRouter.delete('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.deleteApiKeys); diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index 57aa40b..4c8cae4 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -1,11 +1,14 @@ import { User } from '@typings-ce'; import { inject, injectable } from 'inversify'; import { PasswordHelper } from '../helpers/index.js'; -import { UserService } from './index.js'; +import { MeetStorageService, UserService } from './index.js'; @injectable() export class AuthService { - constructor(@inject(UserService) protected userService: UserService) {} + constructor( + @inject(UserService) protected userService: UserService, + @inject(MeetStorageService) protected storageService: MeetStorageService + ) {} async authenticate(username: string, password: string): Promise { const user = await this.userService.getUser(username); @@ -16,4 +19,20 @@ export class AuthService { return user; } + + async createApiKey() { + const apiKey = PasswordHelper.generateApiKey(); + await this.storageService.saveApiKey(apiKey); + return apiKey; + } + + async getApiKeys() { + const apiKeys = await this.storageService.getApiKeys(); + return apiKeys; + } + + async deleteApiKeys() { + await this.storageService.deleteApiKeys(); + return { message: 'API keys deleted successfully' }; + } } diff --git a/backend/src/services/storage/providers/s3/s3-storage-key.builder.ts b/backend/src/services/storage/providers/s3/s3-storage-key.builder.ts index ee58c32..96c8102 100644 --- a/backend/src/services/storage/providers/s3/s3-storage-key.builder.ts +++ b/backend/src/services/storage/providers/s3/s3-storage-key.builder.ts @@ -39,6 +39,10 @@ export class S3KeyBuilder implements StorageKeyBuilder { return `${INTERNAL_CONFIG.S3_USERS_PREFIX}/${userId}.json`; } + buildApiKeysKey(): string { + return `${INTERNAL_CONFIG.S3_API_KEYS_PREFIX}.json`; + } + buildAccessRecordingSecretsKey(recordingId: string): string { const { roomId, egressId, uid } = RecordingHelper.extractInfoFromRecordingId(recordingId); return `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.secrets/${roomId}/${egressId}/${uid}.json`; diff --git a/backend/src/services/storage/storage.interface.ts b/backend/src/services/storage/storage.interface.ts index 71b9717..9e15fda 100644 --- a/backend/src/services/storage/storage.interface.ts +++ b/backend/src/services/storage/storage.interface.ts @@ -151,6 +151,10 @@ export interface StorageKeyBuilder { */ buildUserKey(userId: string): string; - buildAccessRecordingSecretsKey(recordingId:string): string; + /** + * Builds Api Key + */ + buildApiKeysKey(): string; + buildAccessRecordingSecretsKey(recordingId: string): string; } diff --git a/backend/src/services/storage/storage.service.ts b/backend/src/services/storage/storage.service.ts index 64b6c7f..d8cceaa 100644 --- a/backend/src/services/storage/storage.service.ts +++ b/backend/src/services/storage/storage.service.ts @@ -568,6 +568,39 @@ export class MeetStorageService< return await this.saveCacheAndStorage(userRedisKey, storageUserKey, user); } + async saveApiKey(apiKeyData: { key: string; creationDate: string }): Promise { + const redisKey = RedisKeyName.API_KEYS; + const storageKey = this.keyBuilder.buildApiKeysKey(); + this.logger.debug(`Saving API key to Redis and storage: ${redisKey}`); + await this.saveCacheAndStorage(redisKey, storageKey, apiKeyData); + } + + async getApiKeys(): Promise<{ key: string; creationDate: string }[]> { + const redisKey = RedisKeyName.API_KEYS; + const storageKey = this.keyBuilder.buildApiKeysKey(); + this.logger.debug(`Retrieving API key from Redis and storage: ${redisKey}`); + const apiKeys = await this.getFromCacheAndStorage<{ key: string; creationDate: string }[]>( + redisKey, + storageKey + ); + + if (!apiKeys || apiKeys.length === 0) { + this.logger.warn('API key not found in cache or storage'); + return []; + } + + this.logger.debug(`Retrieved API key from storage: ${storageKey}`); + return apiKeys; + } + + async deleteApiKeys(): Promise { + const redisKey = RedisKeyName.API_KEYS; + const storageKey = this.keyBuilder.buildApiKeysKey(); + this.logger.debug(`Deleting API key from Redis and storage: ${redisKey}`); + await this.deleteFromCacheAndStorage(redisKey, storageKey); + this.logger.verbose(`API key deleted successfully from storage: ${storageKey}`); + } + // ========================================== // PRIVATE HELPER METHODS // ========================================== @@ -796,10 +829,6 @@ export class MeetStorageService< } } - // ========================================== - // PRIVATE HELPER METHODS - // ========================================== - /** * Returns the default global preferences. * @returns {GPrefs}