feat: use the same Authorization header for jwt and api key

This commit is contained in:
Ahmed Bouhuolia
2025-07-02 08:30:53 +02:00
parent 5d96357042
commit adb1bea374
6 changed files with 22 additions and 6 deletions

View File

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

View File

@@ -1,4 +1,5 @@
import * as bcrypt from 'bcrypt';
import { AuthApiKeyPrefix } from './Auth.constants';
export const hashPassword = (password: string): Promise<string> =>
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(
{
header: 'x-api-key',
prefix: '',
header: 'Authorization',
prefix: 'Bearer ',
},
false,
);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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<boolean>(
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;