From 376676442ea4a10d9d956f66d9dbe69fb7b88ece Mon Sep 17 00:00:00 2001 From: devlikepro Date: Tue, 5 May 2026 16:23:36 +0700 Subject: [PATCH] [core] Auth - allow using ?x-api-key as auth --- src/core/app.module.core.ts | 7 ++- src/core/auth/HeaderOrQueryApiKeyStrategy.ts | 54 ++++++++++++++++++++ src/core/auth/api-key-auth.middleware.ts | 2 + src/core/auth/apiKey.strategy.ts | 10 ++-- src/nestjs/pipes/WAHAValidationPipe.ts | 13 ++++- src/utils/logging.ts | 12 +++++ 6 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/core/auth/HeaderOrQueryApiKeyStrategy.ts diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index 06568270..28c315f1 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -34,6 +34,7 @@ import { getPinoHttpUseLevel, getPinoLogLevel, getPinoTransport, + redactUrlParams, } from '@waha/utils/logging'; import * as Joi from 'joi'; import { LoggerModule } from 'nestjs-pino'; @@ -91,11 +92,15 @@ export const IMPORTS_CORE = [ ); }, }, + redact: { + paths: ['req.query["x-api-key"]'], + censor: '[REDACTED]', + }, serializers: { req: (req) => ({ id: req.id, method: req.method, - url: req.url, + url: redactUrlParams('x-api-key', req.url, req.query), query: req.query, params: req.params, }), diff --git a/src/core/auth/HeaderOrQueryApiKeyStrategy.ts b/src/core/auth/HeaderOrQueryApiKeyStrategy.ts new file mode 100644 index 00000000..a1e28519 --- /dev/null +++ b/src/core/auth/HeaderOrQueryApiKeyStrategy.ts @@ -0,0 +1,54 @@ +import { Request } from 'express'; +import { Strategy as PassportStrategy } from 'passport-strategy'; + +type VerifyCallback = (err: Error | null, user?: object, info?: object) => void; + +type VerifyFunction = ( + apiKey: string, + verified: VerifyCallback, + req?: Request, +) => void; + +export class HeaderOrQueryApiKeyStrategy extends PassportStrategy { + // Declared here because passport injects these at runtime; the base type omits them. + declare fail: (info: object, status: unknown) => void; + declare error: (err: Error) => void; + declare success: (user: object, info?: object) => void; + + name: string; + passReqToCallback: boolean; + verify: VerifyFunction; + + constructor(passReqToCallback: boolean, verify: VerifyFunction) { + super(); + this.name = 'headerapikey'; + this.passReqToCallback = passReqToCallback; + this.verify = verify; + } + + authenticate(req: Request): void { + const headerKey = req.headers['x-api-key'] as string | undefined; + const queryKey = req.query['x-api-key'] as string | undefined; + const apiKey = headerKey ?? queryKey; + + if (!apiKey) { + return this.fail({ message: 'Missing API Key' }, null); + } + + const verified: VerifyCallback = (err, user?, info?) => { + if (err) { + return this.error(err); + } + if (!user) { + return this.fail(info, null); + } + this.success(user, info); + }; + + if (this.passReqToCallback) { + this.verify(apiKey, verified, req); + } else { + this.verify(apiKey, verified); + } + } +} diff --git a/src/core/auth/api-key-auth.middleware.ts b/src/core/auth/api-key-auth.middleware.ts index 77b22f9b..d01062b2 100644 --- a/src/core/auth/api-key-auth.middleware.ts +++ b/src/core/auth/api-key-auth.middleware.ts @@ -12,6 +12,7 @@ export class ApiKeyAuthMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { if (this.auth instanceof NoAuth) { + delete req.query['x-api-key']; next(); return; } @@ -31,6 +32,7 @@ export class ApiKeyAuthMiddleware implements NestMiddleware { return; } req.user = user; + delete req.query['x-api-key']; next(); })(req, res, next); } diff --git a/src/core/auth/apiKey.strategy.ts b/src/core/auth/apiKey.strategy.ts index f2418204..9feae840 100644 --- a/src/core/auth/apiKey.strategy.ts +++ b/src/core/auth/apiKey.strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { IApiKeyAuth } from '@waha/core/auth/auth'; import { SessionActions } from '@waha/core/auth/casl.types'; -import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; +import { HeaderOrQueryApiKeyStrategy } from '@waha/core/auth/HeaderOrQueryApiKeyStrategy'; import { ApiKeyAuthService } from './ApiKeyAuthService'; export interface User { @@ -19,13 +19,15 @@ function AdminUser(): User { } @Injectable() -export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) { +export class ApiKeyStrategy extends PassportStrategy( + HeaderOrQueryApiKeyStrategy, +) { constructor( private auth: IApiKeyAuth, private apiKeyService: ApiKeyAuthService, ) { - // @ts-ignore - super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => { + // @ts-ignore — PassportStrategy mixin doesn't forward constructor arg types + super(true, (apikey, done) => { return this.validate(apikey, done); }); } diff --git a/src/nestjs/pipes/WAHAValidationPipe.ts b/src/nestjs/pipes/WAHAValidationPipe.ts index f18bd28a..db78fc11 100644 --- a/src/nestjs/pipes/WAHAValidationPipe.ts +++ b/src/nestjs/pipes/WAHAValidationPipe.ts @@ -1,4 +1,8 @@ -import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common'; +import { + ArgumentMetadata, + ValidationPipe, + ValidationPipeOptions, +} from '@nestjs/common'; import { parseBool } from '@waha/helpers'; // So we can change it to True during development and testing @@ -12,4 +16,11 @@ export class WAHAValidationPipe extends ValidationPipe { options.forbidNonWhitelisted = WAHA_HTTP_STRICT_MODE; super(options); } + + async transform(value: any, metadata: ArgumentMetadata) { + if (metadata.type === 'query' && value && typeof value === 'object') { + delete value['x-api-key']; + } + return super.transform(value, metadata); + } } diff --git a/src/utils/logging.ts b/src/utils/logging.ts index 5762519d..2a3d6c6b 100644 --- a/src/utils/logging.ts +++ b/src/utils/logging.ts @@ -79,4 +79,16 @@ export function getPinoTransport() { }; } +export function redactUrlParams( + key: string, + url: string, + query: Record, +): string { + const value = query[key]; + if (!value) { + return url; + } + return url.replace(value, '[REDACTED]'); +} + export { getNestJSLogLevels };