From 2c648a4f79079c7224ea73273fdaeb169fcd12fe Mon Sep 17 00:00:00 2001 From: Ian Mustafa Date: Tue, 9 Dec 2025 11:13:36 +0700 Subject: [PATCH] Add signature verification for query string --- src/middlewares/apiKeyAuth.ts | 73 +++++++++++++++++++++++++++++++++++ src/modules/device/router.ts | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/middlewares/apiKeyAuth.ts b/src/middlewares/apiKeyAuth.ts index 3339653..059fed9 100644 --- a/src/middlewares/apiKeyAuth.ts +++ b/src/middlewares/apiKeyAuth.ts @@ -161,4 +161,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') + } + }, + }), }) diff --git a/src/modules/device/router.ts b/src/modules/device/router.ts index 1ac4c2c..9fd108e 100644 --- a/src/modules/device/router.ts +++ b/src/modules/device/router.ts @@ -49,7 +49,7 @@ export const router = new Elysia({ return { status: 'internal_error', message: 'Server error' } } }, { - verifyKey: true, + verifyKeyQuery: true, query: deviceQueryParams, response: deviceQueryResponseSchema, detail: {