From ca666f7c562f4f585a7d6c7b1322dd6d1da71bb3 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 23 Nov 2025 09:58:08 -0800 Subject: [PATCH] feat(basic auth): Allowing basic authentication for API access --- .env-local | 20 +++++++++--- bin/server.js | 42 ++++++++++++++++++++----- src/config/env/common.js | 7 +++++ src/config/x402.js | 7 +++++ src/middleware/basic-auth.js | 61 ++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 src/middleware/basic-auth.js diff --git a/.env-local b/.env-local index 92b69cf..2108342 100644 --- a/.env-local +++ b/.env-local @@ -1,13 +1,25 @@ +# START INFRASTRUCTURE SETUP + # Full Node Connection RPC_BASEURL=http://172.17.0.1:8332 RPC_USERNAME=bitcoin RPC_PASSWORD=password -# x402 payments required to access this API? -X402_ENABLED=false - # Fulcrum Indexer FULCRUM_API=http://192.168.2.127:3001 # SLP Indexer -SLP_INDEXER_API=http://192.168.2.127:5010 \ No newline at end of file +SLP_INDEXER_API=http://192.168.2.127:5010 + +# END INFRASTRUCTURE SETUP + + +# START ACCESS CONTROL + +# x402 payments required to access this API? +X402_ENABLED=false + +# Basic Authentication required to access this API? +USE_BASIC_AUTH=false + +# END ACCESS CONTROL \ No newline at end of file diff --git a/bin/server.js b/bin/server.js index 323db79..d9b3900 100644 --- a/bin/server.js +++ b/bin/server.js @@ -15,7 +15,8 @@ import { dirname, join } from 'path' import config from '../src/config/index.js' import Controllers from '../src/controllers/index.js' import wlogger from '../src/adapters/wlogger.js' -import { buildX402Routes, getX402Settings } from '../src/config/x402.js' +import { buildX402Routes, getX402Settings, getBasicAuthSettings } from '../src/config/x402.js' +import { basicAuthMiddleware } from '../src/middleware/basic-auth.js' // Load environment variables dotenv.config() @@ -60,6 +61,7 @@ class Server { const app = express() const x402Settings = getX402Settings() + const basicAuthSettings = getBasicAuthSettings() // MIDDLEWARE START app.use(express.json()) @@ -72,22 +74,46 @@ class Server { allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] })) - // Wrap all endpoints in x402 middleware. This handles payments for the API calls. - if (x402Settings.enabled) { + // Apply basic auth middleware if enabled + // This must run before x402 middleware to set req.locals.basicAuthValid + if (basicAuthSettings.enabled) { + wlogger.info('Basic auth middleware enabled') + app.use(basicAuthMiddleware) + } + + // Apply x402 middleware based on configuration + // Logic: + // - If X402_ENABLED=false OR USE_BASIC_AUTH=false: Don't apply x402 (no rate limits) + // - If X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally (bypass if basic auth valid) + + // Only apply x402 if both are enabled + if (x402Settings.enabled && basicAuthSettings.enabled) { + // X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally const routes = buildX402Routes(this.config.apiPrefix) const facilitatorOptions = x402Settings.facilitatorUrl ? { url: x402Settings.facilitatorUrl } : undefined - wlogger.info(`x402 middleware enabled; enforcing ${x402Settings.priceSat} satoshis per request`) - app.use( - x402PaymentMiddleware( + wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceSat} satoshis per request (unless basic auth provided)`) + + // Create conditional x402 middleware that bypasses if basic auth is valid + const conditionalX402Middleware = (req, res, next) => { + // If basic auth is valid, bypass x402 + if (req.locals?.basicAuthValid === true) { + return next() + } + + // Otherwise, apply x402 middleware + return x402PaymentMiddleware( x402Settings.serverAddress, routes, facilitatorOptions - ) - ) + )(req, res, next) + } + + app.use(conditionalX402Middleware) } else { + // X402_ENABLED=false OR USE_BASIC_AUTH=false: No x402 middleware wlogger.info('x402 middleware disabled via configuration') } diff --git a/src/config/env/common.js b/src/config/env/common.js index b11a36a..aab10c1 100644 --- a/src/config/env/common.js +++ b/src/config/env/common.js @@ -38,6 +38,11 @@ const x402Defaults = { priceSat } +const basicAuthDefaults = { + enabled: normalizeBoolean(process.env.USE_BASIC_AUTH, false), + token: process.env.BASIC_AUTH_TOKEN || '' +} + export default { // Server port port: process.env.PORT || 5942, @@ -80,6 +85,8 @@ export default { x402: x402Defaults, + basicAuth: basicAuthDefaults, + // Version version } diff --git a/src/config/x402.js b/src/config/x402.js index 0d2d87f..45f0ac8 100644 --- a/src/config/x402.js +++ b/src/config/x402.js @@ -41,3 +41,10 @@ export function getX402Settings () { priceSat: config.x402?.priceSat } } + +export function getBasicAuthSettings () { + return { + enabled: Boolean(config.basicAuth?.enabled), + token: config.basicAuth?.token || '' + } +} diff --git a/src/middleware/basic-auth.js b/src/middleware/basic-auth.js new file mode 100644 index 0000000..5c9f2e9 --- /dev/null +++ b/src/middleware/basic-auth.js @@ -0,0 +1,61 @@ +/* + Basic Authentication Middleware + + This middleware validates Bearer tokens from the Authorization header. + When a valid token is provided, it sets req.locals.basicAuthValid = true + to allow bypassing x402 middleware. +*/ + +import config from '../config/index.js' +import wlogger from '../adapters/wlogger.js' + +/** + * Middleware function that validates Bearer token authentication + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + */ +export function basicAuthMiddleware (req, res, next) { + // Initialize req.locals if it doesn't exist + if (!req.locals) { + req.locals = {} + } + + // Default to false + req.locals.basicAuthValid = false + + // Get the configured token + const configuredToken = config.basicAuth?.token + + // If no token is configured, skip validation + if (!configuredToken) { + wlogger.warn('Basic auth enabled but no BASIC_AUTH_TOKEN configured') + return next() + } + + // Get the Authorization header + const authHeader = req.headers.authorization + + // If no Authorization header, continue (x402 will handle unauthorized requests) + if (!authHeader) { + return next() + } + + // Check if it's a Bearer token + const parts = authHeader.split(' ') + if (parts.length !== 2 || parts[0] !== 'Bearer') { + return next() + } + + const providedToken = parts[1] + + // Compare tokens + if (providedToken === configuredToken) { + req.locals.basicAuthValid = true + wlogger.verbose(`Basic auth validated for request to ${req.path}`) + } + + // Always continue to next middleware + // If auth failed, x402 middleware will handle the request + next() +}