Files
edr-platform/apps/edr-payment-api/src/common/guards/service-auth.guard.ts
2026-06-11 15:25:24 +03:00

56 lines
1.8 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { timingSafeEqual } from "node:crypto";
import { Request } from "express";
/**
* Shared-secret service-to-service auth for the internal surface (/payments/*).
* Callers send `x-service-token: <SERVICE_AUTH_TOKEN>` (or `Authorization: Bearer …`).
* Webhook endpoints are intentionally NOT behind this guard — they are provider-facing and
* authenticate via signature verification instead.
*/
@Injectable()
export class ServiceAuthGuard implements CanActivate {
private readonly logger = new Logger(ServiceAuthGuard.name);
private readonly token: string;
private warned = false;
constructor(config: ConfigService) {
this.token = config.get<string>("app.serviceAuthToken") ?? "";
if (!this.token && process.env.NODE_ENV === "production") {
throw new Error("SERVICE_AUTH_TOKEN must be set in production");
}
}
canActivate(context: ExecutionContext): boolean {
if (!this.token) {
if (!this.warned) {
this.logger.warn(
"SERVICE_AUTH_TOKEN unset — internal endpoints are UNGUARDED (dev only)",
);
this.warned = true;
}
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const header = request.headers["x-service-token"];
const bearer = request.headers.authorization?.replace(/^Bearer\s+/i, "");
const presented =
(Array.isArray(header) ? header[0] : header) ?? bearer ?? "";
const expected = Buffer.from(this.token);
const actual = Buffer.from(presented);
const valid =
expected.length === actual.length && timingSafeEqual(expected, actual);
if (!valid) throw new UnauthorizedException("Invalid service token");
return true;
}
}