@sourceloop/ctrl-plane-tenant-management-service
Version:
Tenant Management microservice for SaaS control plane
96 lines (89 loc) • 3.22 kB
text/typescript
import {
Interceptor,
InvocationContext,
Provider,
Setter,
ValueOrPromise,
inject,
service,
} from '@loopback/core';
import {WebhookConfig, WebhookPayload} from '../types';
import {HttpErrors, RequestContext} from '@loopback/rest';
import {SYSTEM_USER, WEBHOOK_CONFIG} from '../keys';
import {CryptoHelperService} from '../services';
import {repository} from '@loopback/repository';
import {WebhookSecretRepository} from '../repositories';
import {ILogger, LOGGER} from '@sourceloop/core';
import {timingSafeEqual} from 'crypto';
import {AuthenticationBindings, IAuthUser} from 'loopback4-authentication';
export class WebhookVerifierProvider implements Provider<Interceptor> {
constructor(
(WEBHOOK_CONFIG)
private readonly webhookConfig: WebhookConfig,
(CryptoHelperService)
private readonly cryptoHelperService: CryptoHelperService,
(WebhookSecretRepository)
private readonly webhookSecretRepo: WebhookSecretRepository,
(LOGGER.LOGGER_INJECT)
private readonly logger: ILogger,
.setter(AuthenticationBindings.CURRENT_USER)
private readonly setCurrentUser: Setter<IAuthUser>,
(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: WebhookPayload = request.body;
const timestamp = Number(
request.headers[this.webhookConfig.timestampHeaderName],
);
if (isNaN(timestamp)) {
this.logger.error('Invalid timestamp');
throw new HttpErrors.Unauthorized();
}
const signature = request.headers[this.webhookConfig.signatureHeaderName];
if (!signature || typeof signature !== 'string') {
this.logger.error('Missing signature string');
throw new HttpErrors.Unauthorized();
}
const initiatorId = value.initiatorId;
const secretInfo = await this.webhookSecretRepo.get(initiatorId);
if (!secretInfo) {
this.logger.error('No secret found for this initiator');
throw new HttpErrors.Unauthorized();
}
const expectedSignature =
this.cryptoHelperService.generateHmacForWebhookVerification(
`${JSON.stringify(value)}${secretInfo.context}`,
timestamp,
secretInfo.secret,
);
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 TIMESTAMP_TOLERANCE_MS = 20000; // 20 seconds
// timestamp should be within 5-20 seconds
if (Math.abs(timestamp - Date.now()) > TIMESTAMP_TOLERANCE_MS) {
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();
}
}