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') + } + }, + }), })