UNPKG

@sourceloop/ctrl-plane-tenant-management-service

Version:

Tenant Management microservice for SaaS control plane

101 lines (88 loc) 3.08 kB
import { Interceptor, InvocationContext, Provider, Setter, ValueOrPromise, inject, } from '@loopback/core'; import {AnyObject, repository} from '@loopback/repository'; import {HttpErrors, RequestContext} from '@loopback/rest'; import {ILogger, LOGGER} from '@sourceloop/core'; import {createHmac, timingSafeEqual} from 'crypto'; import {AuthenticationBindings, IAuthUser} from 'loopback4-authentication'; import {SYSTEM_USER} from '../keys'; import {WebhookSecretRepository} from '../repositories'; const DEFAULT_TIME_TOLERANCE = 20000; export class CallbackVerifierProvider implements Provider<Interceptor> { constructor( @inject(LOGGER.LOGGER_INJECT) private readonly logger: ILogger, @repository(WebhookSecretRepository) private readonly webhookSecretRepo: WebhookSecretRepository, @inject.setter(AuthenticationBindings.CURRENT_USER) private readonly setCurrentUser: Setter<IAuthUser>, @inject(SYSTEM_USER) private readonly systemUser: IAuthUser, ) {} value() { return this.intercept.bind(this); } async intercept<T>( invocationCtx: InvocationContext, next: () => ValueOrPromise<T>, ) { const {request} = invocationCtx.parent as RequestContext; const value: AnyObject = request.body; const TIMESTAMP_TOLERANCE = +DEFAULT_TIME_TOLERANCE; const timestamp = Number(request.headers['x-timestamp']); if (isNaN(timestamp)) { this.logger.error('Invalid timestamp'); throw new HttpErrors.Unauthorized(); } const signature = request.headers['x-signature']; if (!signature || typeof signature !== 'string') { this.logger.error('Missing signature string'); throw new HttpErrors.Unauthorized(); } const tenantId = value.tenant?.id; if (!tenantId) { this.logger.error('Missing secret'); throw new HttpErrors.Unauthorized(); } const secretInfo = await this.webhookSecretRepo.get(tenantId); if (!secretInfo) { this.logger.error('No secret found for this initiator'); throw new HttpErrors.Unauthorized(); } const expectedSignature = createHmac('sha256', secretInfo.secret) .update(`${JSON.stringify(value)}${timestamp}`) .digest('hex'); try { // actual signature should be equal to expected signature // timing safe equal is used to prevent timing attacks if ( !timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature)) ) { this.logger.error('Invalid signature'); throw new HttpErrors.Unauthorized(); } const hh = Math.abs(timestamp - Date.now()); // timestamp should be within 20 seconds if (hh > TIMESTAMP_TOLERANCE) { this.logger.error('Timestamp out of tolerance'); throw new HttpErrors.Unauthorized(); } } catch (e) { this.logger.error(e); throw new HttpErrors.Unauthorized(); } this.setCurrentUser(this.systemUser); return next(); } } export type TempUser = { userTenantId: string; tenantType: string; tenantId?: string; } & IAuthUser;