diff --git a/src/middlewares/apiKeyAuth.ts b/src/middlewares/apiKeyAuth.ts index 059fed9..81c82e9 100644 --- a/src/middlewares/apiKeyAuth.ts +++ b/src/middlewares/apiKeyAuth.ts @@ -94,6 +94,97 @@ export const apiKeyAuthMacro = new Elysia().macro({ if (!enabled) { return } +<<<<<<< HEAD +======= + const apiKey = headers['x-api-key'] + const nonce = headers['x-nonce'] + const signature = headers['x-signature'] + const rawTimestamp = headers['x-timestamp'] + + 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 (!apiKey || !nonce || !signature || !rawTimestamp) { + const missing = missingHeaders.join(', ') + logger.debug({ + 'headers.authorization': headers.authorization, + }, 'Failed API key authentication attempt due to missing auth headers: ' + missing) + throw new UnauthenticatedError('Missing auth headers: ' + missing) + } + + 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(body, 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') + } + }, + }), + verifyKeyQuery: (enabled: boolean) => ({ + async beforeHandle({ headers, query }) { + if (!enabled) { + return + } + +>>>>>>> feature/auth const apiKey = headers['x-api-key'] const nonce = headers['x-nonce'] const signature = headers['x-signature'] @@ -153,7 +244,7 @@ export const apiKeyAuthMacro = new Elysia().macro({ }, 'Failed to save merchant nonce into database') }) - if (!verify(body, timestamp, nonce, signature, merchant.secretKey)) { + if (!verify(query, timestamp, nonce, signature, merchant.secretKey)) { logger.debug({ 'headers.authorization': headers.authorization, }, 'Failed API key authentication attempt due to invalid signature')