Merge branch 'api-keys' into develop

This commit is contained in:
Ahmed Bouhuolia
2025-07-02 08:31:21 +02:00
6 changed files with 22 additions and 6 deletions

View File

@@ -26,3 +26,5 @@ export const SendResetPasswordMailJob = 'SendResetPasswordMailJob';
export const SendSignupVerificationMailQueue = export const SendSignupVerificationMailQueue =
'SendSignupVerificationMailQueue'; 'SendSignupVerificationMailQueue';
export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob'; export const SendSignupVerificationMailJob = 'SendSignupVerificationMailJob';
export const AuthApiKeyPrefix = 'bc_';

View File

@@ -1,4 +1,5 @@
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { AuthApiKeyPrefix } from './Auth.constants';
export const hashPassword = (password: string): Promise<string> => export const hashPassword = (password: string): Promise<string> =>
new Promise((resolve) => { new Promise((resolve) => {
@@ -8,3 +9,12 @@ export const hashPassword = (password: string): Promise<string> =>
}); });
}); });
}); });
/**
* 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 : '';
};

View File

@@ -13,8 +13,8 @@ export class ApiKeyStrategy extends PassportStrategy(
) { ) {
super( super(
{ {
header: 'x-api-key', header: 'Authorization',
prefix: '', prefix: 'Bearer ',
}, },
false, false,
); );

View File

@@ -1,6 +1,7 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtAuthGuard } from '../guards/jwt.guard'; import { JwtAuthGuard } from '../guards/jwt.guard';
import { ApiKeyAuthGuard } from './AuthApiKey.guard'; import { ApiKeyAuthGuard } from './AuthApiKey.guard';
import { getAuthApiKey } from '../Auth.utils';
// mixed-auth.guard.ts // mixed-auth.guard.ts
@Injectable() @Injectable()
@@ -12,7 +13,7 @@ export class MixedAuthGuard implements CanActivate {
canActivate(context: ExecutionContext) { canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key']; const apiKey = getAuthApiKey(request.headers['authorization'] || '');
if (apiKey) { if (apiKey) {
return this.apiKeyGuard.canActivate(context); return this.apiKeyGuard.canActivate(context);

View File

@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { ApiKeyModel } from '../models/ApiKey.model'; import { ApiKeyModel } from '../models/ApiKey.model';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { AuthApiKeyPrefix } from '../Auth.constants';
@Injectable() @Injectable()
export class GenerateApiKey { export class GenerateApiKey {
@@ -21,7 +22,7 @@ export class GenerateApiKey {
const user = await this.tenancyContext.getSystemUser(); const user = await this.tenancyContext.getSystemUser();
// Generate a secure random API key // 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 // Save the API key to the database
const apiKeyRecord = await this.apiKeyModel.query().insert({ const apiKeyRecord = await this.apiKeyModel.query().insert({
key, key,

View File

@@ -8,6 +8,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants'; import { IS_PUBLIC_ROUTE } from '../Auth/Auth.constants';
import { getAuthApiKey } from '../Auth/Auth.utils';
export const IS_TENANT_AGNOSTIC = 'IS_TENANT_AGNOSTIC'; export const IS_TENANT_AGNOSTIC = 'IS_TENANT_AGNOSTIC';
@@ -26,6 +27,7 @@ export class TenancyGlobalGuard implements CanActivate {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const organizationId = request.headers['organization-id']; const organizationId = request.headers['organization-id'];
const authorization = request.headers['authorization']?.trim(); const authorization = request.headers['authorization']?.trim();
const isAuthApiKey = !!getAuthApiKey(authorization || '');
const isPublic = this.reflector.getAllAndOverride<boolean>( const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_ROUTE, IS_PUBLIC_ROUTE,
@@ -35,10 +37,10 @@ export class TenancyGlobalGuard implements CanActivate {
IS_TENANT_AGNOSTIC, IS_TENANT_AGNOSTIC,
[context.getHandler(), context.getClass()], [context.getHandler(), context.getClass()],
); );
if (isPublic || isTenantAgnostic) { if (isPublic || isTenantAgnostic || isAuthApiKey) {
return true; return true;
} }
if (!isEmpty(authorization) && !organizationId) { if (!organizationId) {
throw new UnauthorizedException('Organization ID is required.'); throw new UnauthorizedException('Organization ID is required.');
} }
return true; return true;