@sourceloop/ctrl-plane-tenant-management-service
Version:
Tenant Management microservice for SaaS control plane
101 lines (88 loc) • 3.08 kB
text/typescript
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(
(LOGGER.LOGGER_INJECT)
private readonly logger: ILogger,
(WebhookSecretRepository)
private readonly webhookSecretRepo: WebhookSecretRepository,
.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: 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;