import Elysia, { HTTPHeaders, StatusMap } from 'elysia' import { db, table as $t } from '~/db' import { and, eq } from 'drizzle-orm' import { apiAuth } from '~/config' import merchantNonces from '~/db/schema/merchant_nonces' import { verify } from '~/helpers/signature' import { UnauthenticatedError } from '~/helpers/errors' import { logger } from '~/plugins' export const apiKeyAuthMiddleware = async ( headers: Record, body: unknown, set: { headers: HTTPHeaders status?: number | keyof StatusMap }, ) => { 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') set.status = 401 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') set.status = 401 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') set.status = 401 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') set.status = 401 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 request replay detected') set.status = 401 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') set.status = 401 throw new UnauthenticatedError('Invalid signature') } } export const apiKeyAuthMacro = new Elysia().macro({ verifyKey: (enabled: boolean) => ({ async beforeHandle({ headers, body }) { if (!enabled) { return } 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 } 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') } }, }), // 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'] // 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(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') // } // }, // }), })