From ea2ca86ce7fd2792b4c4c138e3e5887f80734e7d Mon Sep 17 00:00:00 2001 From: Ian Mustafa Date: Tue, 9 Dec 2025 12:14:20 +0700 Subject: [PATCH 1/2] Add verbosity to missing auth headers error --- src/middlewares/apiKeyAuth.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/middlewares/apiKeyAuth.ts b/src/middlewares/apiKeyAuth.ts index d1b451b..fdaeff3 100644 --- a/src/middlewares/apiKeyAuth.ts +++ b/src/middlewares/apiKeyAuth.ts @@ -99,11 +99,26 @@ export const apiKeyAuthMacro = new Elysia().macro({ const nonce = headers['x-nonce'] const signature = headers['x-signature'] const rawTimestamp = headers['x-timestamp'] - if (!apiKey || !nonce || !signature || !rawTimestamp) { + + const missingHeaders = [] + if (!apiKey) { + missingHeaders.push('X-API-Key') + } + if (!nonce) { + missingHeaders.push('X-Nonce') + } + if (!rawTimestamp) { + missingHeaders.push('X-Timestamp') + } + if (!signature) { + missingHeaders.push('X-Signature') + } + if (missingHeaders.length) { + const missing = missingHeaders.join(', ') logger.debug({ 'headers.authorization': headers.authorization, - }, 'Failed API key authentication attempt due to missing auth headers') - throw new UnauthenticatedError('Missing auth headers') + }, 'Failed API key authentication attempt due to missing auth headers: ' + missing) + throw new UnauthenticatedError('Missing auth headers: ' + missing) } const timestamp = Number(rawTimestamp) From 36a213b6d22b92df92456b70ea313611fa01b313 Mon Sep 17 00:00:00 2001 From: Ian Mustafa Date: Tue, 9 Dec 2025 12:20:13 +0700 Subject: [PATCH 2/2] Add verbosity to missing auth headers error (#2) --- src/middlewares/apiKeyAuth.ts | 76 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/middlewares/apiKeyAuth.ts b/src/middlewares/apiKeyAuth.ts index fdaeff3..e815478 100644 --- a/src/middlewares/apiKeyAuth.ts +++ b/src/middlewares/apiKeyAuth.ts @@ -94,7 +94,6 @@ export const apiKeyAuthMacro = new Elysia().macro({ if (!enabled) { return } - const apiKey = headers['x-api-key'] const nonce = headers['x-nonce'] const signature = headers['x-signature'] @@ -113,7 +112,7 @@ export const apiKeyAuthMacro = new Elysia().macro({ if (!signature) { missingHeaders.push('X-Signature') } - if (missingHeaders.length) { + if (!apiKey || !nonce || !signature || !rawTimestamp) { const missing = missingHeaders.join(', ') logger.debug({ 'headers.authorization': headers.authorization, @@ -177,4 +176,77 @@ export const apiKeyAuthMacro = new Elysia().macro({ } }, }), + verifyKeyQuery: (enabled: boolean) => ({ + async beforeHandle({ headers, query }) { + if (!enabled) { + return + } + + const apiKey = headers['x-api-key'] + const nonce = headers['x-nonce'] + const signature = headers['x-signature'] + const rawTimestamp = headers['x-timestamp'] + if (!apiKey || !nonce || !signature || !rawTimestamp) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to missing auth headers') + throw new UnauthenticatedError('Missing auth headers') + } + + const timestamp = Number(rawTimestamp) + if (!Number.isFinite(timestamp)) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to invalid timestamp') + throw new UnauthenticatedError('Invalid timestamp') + } + const nowSec = Math.floor(Date.now() / 1000) + if (Math.abs(nowSec - timestamp) > apiAuth.skewSeconds) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to timestamp out of range') + throw new UnauthenticatedError('Timestamp out of range') + } + + const merchant = await db.query.merchants.findFirst({ + where: (t, { and, eq }) => and( + eq(t.apiKey, apiKey), + eq(t.isActive, true), + ), + }) + if (!merchant) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to invalid API key') + throw new UnauthenticatedError('Invalid API key') + } + + const existingNonce = await db.$count($t.merchantNonces, and( + eq($t.merchantNonces.merchantId, merchant.merchantId), + eq($t.merchantNonces.nonce, nonce), + )) + if (existingNonce > 0) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to replay attack detected') + throw new UnauthenticatedError('Duplicate request detected, ensure your nonce is unique') + } + + db.insert(merchantNonces).values({ + merchantId: merchant.merchantId, + nonce, + }).catch((error) => { + logger.error({ + errorMessage: error.message, + }, 'Failed to save merchant nonce into database') + }) + + if (!verify(query, timestamp, nonce, signature, merchant.secretKey)) { + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to invalid signature') + throw new UnauthenticatedError('Invalid signature') + } + }, + }), })