341 lines
12 KiB
TypeScript
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')
|
|
// }
|
|
// },
|
|
// }),
|
|
})
|