diff --git a/src/core/auth/WebSocketAuth.ts b/src/core/auth/WebSocketAuth.ts new file mode 100644 index 00000000..c8fa40e9 --- /dev/null +++ b/src/core/auth/WebSocketAuth.ts @@ -0,0 +1,30 @@ +import * as url from 'url'; + +import { validateApiKey } from './apiKey.strategy'; + +export class WebSocketAuth { + private key: string; + + constructor() { + this.key = process.env.WHATSAPP_API_KEY || ''; + } + + verifyClient = (info: any, callback: any) => { + if (!this.key) { + callback(true); + return; + } + // Do something with the info + let query = url.parse(info.req.url, true).query; + // case insensitive + query = Object.keys(query).reduce((acc, key) => { + acc[key.toLowerCase()] = query[key]; + return acc; + }, {}); + + const apiKey = query['x-api-key']; + // @ts-ignore + const isValid = validateApiKey(apiKey, this.key); + callback(isValid); + }; +} diff --git a/src/core/auth/apiKey.strategy.ts b/src/core/auth/apiKey.strategy.ts new file mode 100644 index 00000000..823945bd --- /dev/null +++ b/src/core/auth/apiKey.strategy.ts @@ -0,0 +1,56 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import * as crypto from 'crypto'; +import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; + +import { WhatsappConfigService } from '../../config.service'; + +/** + * Securely validates API key using constant-time comparison + * @param providedKey The key provided in the request + * @param storedKey The key stored in the configuration + * @returns boolean indicating if the keys match + */ +export function validateApiKey( + providedKey: string, + storedKey: string | undefined, +): boolean { + if (!storedKey || !providedKey) { + return false; + } + + try { + // Convert strings to buffers for constant-time comparison + const providedKeyBuffer = Buffer.from(providedKey); + const storedKeyBuffer = Buffer.from(storedKey); + + // If lengths are different, return false but use a dummy comparison to prevent timing attacks + if (providedKeyBuffer.length !== storedKeyBuffer.length) { + // Create a dummy buffer of the same length as the provided key + const dummyBuffer = Buffer.alloc(providedKeyBuffer.length); + // Perform comparison with dummy buffer to maintain constant time + crypto.timingSafeEqual(providedKeyBuffer, dummyBuffer); + return false; + } + + // Perform constant-time comparison + return crypto.timingSafeEqual(providedKeyBuffer, storedKeyBuffer); + } catch (error) { + return false; + } +} + +@Injectable() +export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) { + constructor(private config: WhatsappConfigService) { + super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => { + const isValid = validateApiKey(apikey, this.config.getApiKey()); + return done(isValid); + }); + } + + validate(apikey: string, done: (result: boolean) => void): void { + const isValid = validateApiKey(apikey, this.config.getApiKey()); + return done(isValid); + } +} diff --git a/src/core/auth/auth.middleware.ts b/src/core/auth/auth.middleware.ts new file mode 100644 index 00000000..15708b80 --- /dev/null +++ b/src/core/auth/auth.middleware.ts @@ -0,0 +1,28 @@ +import { + Injectable, + NestMiddleware, + UnauthorizedException, +} from '@nestjs/common'; +import * as passport from 'passport'; + +import { WhatsappConfigService } from '../../config.service'; + +@Injectable() +export class AuthMiddleware implements NestMiddleware { + constructor(private config: WhatsappConfigService) {} + + use(req: any, res: any, next: () => void) { + // No api key - skip the validation path + if (!this.config.getApiKey()) { + next(); + return; + } + + passport.authenticate('headerapikey', { session: false }, (value) => { + if (!value) { + throw new UnauthorizedException(); + } + next(); + })(req, res, next); + } +} diff --git a/src/core/auth/basicAuth.ts b/src/core/auth/basicAuth.ts new file mode 100644 index 00000000..38774f56 --- /dev/null +++ b/src/core/auth/basicAuth.ts @@ -0,0 +1,21 @@ +import * as basicAuth from 'express-basic-auth'; + +export function BasicAuthFunction(username, password, exclude: string[] = []) { + function authFunction(req, res, next) { + const ignore = exclude.filter((url) => req.url.startsWith(url)).length > 0; + if (ignore) { + next(); + return; + } + + const auth = basicAuth({ + challenge: true, + users: { + [username]: password, + }, + }); + auth(req, res, next); + } + + return authFunction; +}