From 5f1b3aee468a1b440ccbdf2900491cba8fccd80a Mon Sep 17 00:00:00 2001 From: smartdev Date: Sat, 17 Oct 2026 16:26:27 +0100 Subject: [PATCH] Fix #607,608,610,611 --- src/lib/admin-auth-helper.ts | 33 +++++++++++++++++++++++++ src/middleware/admin-auth.middleware.ts | 4 +-- src/middleware/auth.middleware.ts | 16 +++++++++++- src/routes/admin/approve-invoice.ts | 4 +-- src/routes/admin/approve-kyc.ts | 4 +-- 5 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 src/lib/admin-auth-helper.ts diff --git a/src/lib/admin-auth-helper.ts b/src/lib/admin-auth-helper.ts new file mode 100644 index 00000000..05788195 --- /dev/null +++ b/src/lib/admin-auth-helper.ts @@ -0,0 +1,33 @@ +import crypto from "crypto"; +import type { Request } from "express"; +import type { AuthenticatedRequest } from "@/types/auth"; + +/** + * Timing-safe string comparison (#607). + */ +export function isTimingSafeEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); +} + +/** + * Validates admin authentication via either valid admin JWT or x-admin-key (#607, #608). + */ +export function isValidAdminAuth(req: Request): boolean { + const authReq = req as AuthenticatedRequest; + if (authReq.user && authReq.user.role === "admin") { + return true; + } + + const suppliedKey = req.headers["x-admin-key"]; + const expectedKey = process.env.ADMIN_API_KEY; + + if (typeof suppliedKey === "string" && typeof expectedKey === "string" && expectedKey.length > 0) { + if (expectedKey === "dev-admin-key" && process.env.NODE_ENV === "production") { + return false; // Reject weak default keys in production + } + return isTimingSafeEqual(suppliedKey, expectedKey); + } + + return false; +} diff --git a/src/middleware/admin-auth.middleware.ts b/src/middleware/admin-auth.middleware.ts index 473d4e65..bad78739 100644 --- a/src/middleware/admin-auth.middleware.ts +++ b/src/middleware/admin-auth.middleware.ts @@ -16,7 +16,7 @@ interface AdminTokenPayload { export function authenticateAdminJWT(req: Request, _res: Response, next: NextFunction): void { const authHeader = req.headers.authorization; - if (!authHeader?.startsWith("Bearer ")) { + if (!authHeader || !/^bearer\s+/i.test(authHeader)) { next( new HttpError( 401, @@ -27,7 +27,7 @@ export function authenticateAdminJWT(req: Request, _res: Response, next: NextFun return; } - const token = authHeader.slice(7); + const token = authHeader.replace(/^bearer\s+/i, ""); try { const secret = process.env.ADMIN_JWT_SECRET ?? process.env.JWT_SECRET; diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 8508ccdc..8bfff6bd 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -268,11 +268,25 @@ export async function authenticateJWT(req: Request, _res: Response, next: NextFu } const { userId, stellarAddress, wallet, userType, role } = claims as Partial; + const resolvedAddress = nonEmptyString(wallet) ?? nonEmptyString(stellarAddress) ?? subject; + + // Issue #610: Check wallet suspension status + const { isWalletSuspended } = await import("@/middleware/suspended-wallet.middleware"); + if (isWalletSuspended(resolvedAddress)) { + next( + new HttpError( + 403, + "Wallet is suspended and cannot perform authenticated actions.", + buildAuthFailureDetails(token, "wallet_suspended") + ) + ); + return; + } (req as AuthenticatedRequest).user = { id: nonEmptyString(userId) ?? subject, // Tokens are issued with the wallet address as subject. - stellarAddress: nonEmptyString(wallet) ?? nonEmptyString(stellarAddress) ?? subject, + stellarAddress: resolvedAddress, email: null, userType: userType || role || (null as unknown as UserType), kycStatus: null as unknown as KYCStatus, diff --git a/src/routes/admin/approve-invoice.ts b/src/routes/admin/approve-invoice.ts index 279bc963..01ad38e9 100644 --- a/src/routes/admin/approve-invoice.ts +++ b/src/routes/admin/approve-invoice.ts @@ -2,6 +2,7 @@ import { Request, Response } from "express"; import type { InvoiceService } from "@/services/invoice.service"; import { ServiceError } from "@/utils/service-error"; import { logger } from "@/observability/logger"; +import { isValidAdminAuth } from "@/lib/admin-auth-helper"; interface ApproveInvoiceParams { id: string; @@ -21,8 +22,7 @@ export async function approveInvoice( invoiceService: InvoiceService ) { try { - const adminKey = req.headers["x-admin-key"]; - if (adminKey !== process.env.ADMIN_API_KEY) { + if (!isValidAdminAuth(req)) { return res.status(401).json({ error: "Unauthorized" }); } diff --git a/src/routes/admin/approve-kyc.ts b/src/routes/admin/approve-kyc.ts index 636145a7..179a58ff 100644 --- a/src/routes/admin/approve-kyc.ts +++ b/src/routes/admin/approve-kyc.ts @@ -4,6 +4,7 @@ import { User } from "@/models/User.model"; import { KYCStatus } from "@/types/enums"; import { logKYCStatusChange, logKYCReviewFailure } from "@/lib/kyc-status-log"; import { logger } from "@/observability/logger"; +import { isValidAdminAuth } from "@/lib/admin-auth-helper"; interface ApproveKYCBody { userId: string; @@ -16,10 +17,9 @@ export async function approveKYC( dataSource: DataSource ) { try { - const adminKey = req.headers["x-admin-key"]; const correlationId = req.headers["x-request-id"] as string | undefined; - if (adminKey !== process.env.ADMIN_API_KEY) { + if (!isValidAdminAuth(req)) { logKYCReviewFailure(logger, { userId: req.body?.userId, reviewerId: req.body?.reviewerId,