Files
vega-cloud/src/middlewares/apiKeyAuth.ts
T
2025-12-11 11:17:39 +07:00

341 lines
12 KiB
TypeScript

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<string, string | undefined>,
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')
// }
// },
// }),
})