From adb1bea374b70f679d952e0ad0cc81bb9364c939 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 2 Jul 2025 08:30:53 +0200 Subject: [PATCH] feat: use the same Authorization header for jwt and api key --- packages/server/src/modules/Auth/Auth.constants.ts | 2 ++ packages/server/src/modules/Auth/Auth.utils.ts | 10 ++++++++++ .../src/modules/Auth/api-key/AuthApiKey.strategy.ts | 4 ++-- .../server/src/modules/Auth/api-key/MixedAuth.guard.ts | 3 ++- .../modules/Auth/commands/GenerateApiKey.service.ts | 3 ++- .../server/src/modules/Tenancy/TenancyGlobal.guard.ts | 6 ++++-- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/server/src/modules/Auth/Auth.constants.ts b/packages/server/src/modules/Auth/Auth.constants.ts index d888387c4..6e4c36c9c 100644 --- a/packages/server/src/modules/Auth/Auth.constants.ts +++ b/packages/server/src/modules/Auth/Auth.constants.ts @@ -26,3 +26,5 @@ export const SendResetPasswordMailJob = 'SendResetPasswordMailJob'; export const SendSignupVerificationMailQueue = 'SendSignupVerificationMailQueue'; export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob'; + +export const AuthApiKeyPrefix = 'bc_'; diff --git a/packages/server/src/modules/Auth/Auth.utils.ts b/packages/server/src/modules/Auth/Auth.utils.ts index 38ef94a23..3af19853b 100644 --- a/packages/server/src/modules/Auth/Auth.utils.ts +++ b/packages/server/src/modules/Auth/Auth.utils.ts @@ -1,4 +1,5 @@ import * as bcrypt from 'bcrypt'; +import { AuthApiKeyPrefix } from './Auth.constants'; export const hashPassword = (password: string): Promise => new Promise((resolve) => { @@ -8,3 +9,12 @@ export const hashPassword = (password: string): Promise => }); }); }); + +/** + * Extracts and validates an API key from the Authorization header + * @param {string} authorization - Full authorization header content. + */ +export const getAuthApiKey = (authorization: string) => { + const apiKey = authorization.toLowerCase().replace('bearer ', '').trim(); + return apiKey.startsWith(AuthApiKeyPrefix) ? apiKey : ''; +}; diff --git a/packages/server/src/modules/Auth/api-key/AuthApiKey.strategy.ts b/packages/server/src/modules/Auth/api-key/AuthApiKey.strategy.ts index 4bc968e5b..38e239e04 100644 --- a/packages/server/src/modules/Auth/api-key/AuthApiKey.strategy.ts +++ b/packages/server/src/modules/Auth/api-key/AuthApiKey.strategy.ts @@ -13,8 +13,8 @@ export class ApiKeyStrategy extends PassportStrategy( ) { super( { - header: 'x-api-key', - prefix: '', + header: 'Authorization', + prefix: 'Bearer ', }, false, ); diff --git a/packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts b/packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts index d7d3b0739..a65491672 100644 --- a/packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts +++ b/packages/server/src/modules/Auth/api-key/MixedAuth.guard.ts @@ -1,6 +1,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { JwtAuthGuard } from '../guards/jwt.guard'; import { ApiKeyAuthGuard } from './AuthApiKey.guard'; +import { getAuthApiKey } from '../Auth.utils'; // mixed-auth.guard.ts @Injectable() @@ -12,7 +13,7 @@ export class MixedAuthGuard implements CanActivate { canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); - const apiKey = request.headers['x-api-key']; + const apiKey = getAuthApiKey(request.headers['authorization'] || ''); if (apiKey) { return this.apiKeyGuard.canActivate(context); diff --git a/packages/server/src/modules/Auth/commands/GenerateApiKey.service.ts b/packages/server/src/modules/Auth/commands/GenerateApiKey.service.ts index 5b7ec1788..5f57201b7 100644 --- a/packages/server/src/modules/Auth/commands/GenerateApiKey.service.ts +++ b/packages/server/src/modules/Auth/commands/GenerateApiKey.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as crypto from 'crypto'; import { ApiKeyModel } from '../models/ApiKey.model'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { AuthApiKeyPrefix } from '../Auth.constants'; @Injectable() export class GenerateApiKey { @@ -21,7 +22,7 @@ export class GenerateApiKey { const user = await this.tenancyContext.getSystemUser(); // Generate a secure random API key - const key = crypto.randomBytes(48).toString('hex'); + const key = `${AuthApiKeyPrefix}${crypto.randomBytes(48).toString('hex')}`; // Save the API key to the database const apiKeyRecord = await this.apiKeyModel.query().insert({ key, diff --git a/packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts b/packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts index 97a6f24fe..04310a100 100644 --- a/packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts +++ b/packages/server/src/modules/Tenancy/TenancyGlobal.guard.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; +import { getAuthApiKey } from '../Auth/Auth.utils'; export const IS_TENANT_AGNOSTIC = 'IS_TENANT_AGNOSTIC'; @@ -26,6 +27,7 @@ export class TenancyGlobalGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const organizationId = request.headers['organization-id']; const authorization = request.headers['authorization']?.trim(); + const isAuthApiKey = !!getAuthApiKey(authorization || ''); const isPublic = this.reflector.getAllAndOverride( IS_PUBLIC_ROUTE, @@ -35,10 +37,10 @@ export class TenancyGlobalGuard implements CanActivate { IS_TENANT_AGNOSTIC, [context.getHandler(), context.getClass()], ); - if (isPublic || isTenantAgnostic) { + if (isPublic || isTenantAgnostic || isAuthApiKey) { return true; } - if (!isEmpty(authorization) && !organizationId) { + if (!organizationId) { throw new UnauthorizedException('Organization ID is required.'); } return true;