diff --git a/CLAUDE.md b/CLAUDE.md index 85447df..3c15989 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -566,7 +566,7 @@ For detailed deployment instructions, see [HTTP Deployment Guide](./docs/HTTP_DE **Technical Details**: - `src/http-server-single-session.ts` - Single-session implementation (partial fix) -- `src/http-server-fixed.ts` - Direct JSON-RPC implementation (complete fix) +- `src/http-server.ts` - Direct JSON-RPC implementation (complete fix) - `src/utils/console-manager.ts` - Console output isolation - Use `USE_FIXED_HTTP=true` to enable the fixed implementation diff --git a/data/nodes.db b/data/nodes.db index 017f60b..d86a1cd 100644 Binary files a/data/nodes.db and b/data/nodes.db differ diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f3a09aa..363ee5e 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -2,8 +2,14 @@ set -e # Environment variable validation -if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ]; then - echo "ERROR: AUTH_TOKEN is required for HTTP mode" +if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ] && [ -z "$AUTH_TOKEN_FILE" ]; then + echo "ERROR: AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode" + exit 1 +fi + +# Validate AUTH_TOKEN_FILE if provided +if [ -n "$AUTH_TOKEN_FILE" ] && [ ! -f "$AUTH_TOKEN_FILE" ]; then + echo "ERROR: AUTH_TOKEN_FILE specified but file not found: $AUTH_TOKEN_FILE" exit 1 fi diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f23a0bd..c456085 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.10] - 2025-07-07 + +### Added +- Enhanced authentication logging for better debugging of client authentication issues +- Specific error reasons for authentication failures: `no_auth_header`, `invalid_auth_format`, `invalid_token` +- AUTH_TOKEN_FILE support in single-session HTTP server for consistency +- Empty token validation to prevent security issues +- Whitespace trimming for authentication tokens + +### Fixed +- Issue #22: Improved authentication failure diagnostics for mcp-remote client debugging +- Issue #16: Fixed AUTH_TOKEN_FILE validation for HTTP mode in Docker production stacks - Docker entrypoint now properly validates and supports AUTH_TOKEN_FILE environment variable +- Security: Removed token length from logs to prevent information disclosure + +### Security +- Authentication tokens are now trimmed to handle whitespace edge cases +- Empty tokens are explicitly rejected with clear error messages +- Removed sensitive information (token lengths) from authentication logs + ## [2.7.8] - 2025-07-06 ### Added @@ -26,12 +45,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.7.5] - 2025-07-06 ### Added -- AUTH_TOKEN_FILE support for reading authentication tokens from files (Docker secrets compatible) +- AUTH_TOKEN_FILE support for reading authentication tokens from files (Docker secrets compatible) - partial implementation - Known Issues section in README documenting Claude Desktop container duplication bug - Enhanced authentication documentation in Docker README ### Fixed -- Issue #16: AUTH_TOKEN_FILE was documented but not implemented +- Issue #16: AUTH_TOKEN_FILE was documented but not implemented (partially fixed - see v2.7.10 for complete fix) - HTTP server now properly supports both AUTH_TOKEN and AUTH_TOKEN_FILE environment variables ### Changed @@ -344,6 +363,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Basic n8n and MCP integration - Core workflow automation features +[2.7.10]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.8...v2.7.10 [2.7.8]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.5...v2.7.8 [2.7.5]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.4...v2.7.5 [2.7.4]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.3...v2.7.4 diff --git a/package.json b/package.json index 772a214..9a7e816 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.7.9", + "version": "2.7.10", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "bin": { @@ -41,6 +41,7 @@ "test:tools-documentation": "node dist/scripts/test-tools-documentation.js", "test:mcp:update-partial": "node dist/scripts/test-mcp-n8n-update-partial.js", "test:update-partial:debug": "node dist/scripts/test-update-partial-debug.js", + "test:auth-logging": "tsx scripts/test-auth-logging.ts", "sanitize:templates": "node dist/scripts/sanitize-templates.js", "db:rebuild": "node dist/scripts/rebuild-database.js", "db:init": "node -e \"new (require('./dist/services/sqlite-storage-service').SQLiteStorageService)(); console.log('Database initialized')\"", diff --git a/package.runtime.json b/package.runtime.json index 78130a9..64e906e 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.7.9", + "version": "2.7.10", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/src/http-server-single-session.ts b/src/http-server-single-session.ts index cd9b829..1639c14 100644 --- a/src/http-server-single-session.ts +++ b/src/http-server-single-session.ts @@ -9,6 +9,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import { N8NDocumentationMCPServer } from './mcp/server'; import { ConsoleManager } from './utils/console-manager'; import { logger } from './utils/logger'; +import { readFileSync } from 'fs'; import dotenv from 'dotenv'; dotenv.config(); @@ -25,26 +26,57 @@ export class SingleSessionHTTPServer { private consoleManager = new ConsoleManager(); private expressServer: any; private sessionTimeout = 30 * 60 * 1000; // 30 minutes + private authToken: string | null = null; constructor() { // Validate environment on construction this.validateEnvironment(); } + /** + * Load auth token from environment variable or file + */ + private loadAuthToken(): string | null { + // First, try AUTH_TOKEN environment variable + if (process.env.AUTH_TOKEN) { + logger.info('Using AUTH_TOKEN from environment variable'); + return process.env.AUTH_TOKEN; + } + + // Then, try AUTH_TOKEN_FILE + if (process.env.AUTH_TOKEN_FILE) { + try { + const token = readFileSync(process.env.AUTH_TOKEN_FILE, 'utf-8').trim(); + logger.info(`Loaded AUTH_TOKEN from file: ${process.env.AUTH_TOKEN_FILE}`); + return token; + } catch (error) { + logger.error(`Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`, error); + console.error(`ERROR: Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`); + console.error(error instanceof Error ? error.message : 'Unknown error'); + return null; + } + } + + return null; + } + /** * Validate required environment variables */ private validateEnvironment(): void { - const required = ['AUTH_TOKEN']; - const missing = required.filter(key => !process.env[key]); + // Load auth token from env var or file + this.authToken = this.loadAuthToken(); - if (missing.length > 0) { - const message = `Missing required environment variables: ${missing.join(', ')}`; + if (!this.authToken || this.authToken.trim() === '') { + const message = 'No authentication token found or token is empty. Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token.'; logger.error(message); throw new Error(message); } - if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) { + // Update authToken to trimmed version + this.authToken = this.authToken.trim(); + + if (this.authToken.length < 32) { logger.warn('AUTH_TOKEN should be at least 32 characters for security'); } } @@ -220,16 +252,55 @@ export class SingleSessionHTTPServer { // Main MCP endpoint with authentication app.post('/mcp', async (req: express.Request, res: express.Response): Promise => { - // Simple auth check + // Enhanced authentication check with specific logging const authHeader = req.headers.authorization; - const token = authHeader?.startsWith('Bearer ') - ? authHeader.slice(7) - : authHeader; - if (token !== process.env.AUTH_TOKEN) { - logger.warn('Authentication failed', { + // Check if Authorization header is missing + if (!authHeader) { + logger.warn('Authentication failed: Missing Authorization header', { ip: req.ip, - userAgent: req.get('user-agent') + userAgent: req.get('user-agent'), + reason: 'no_auth_header' + }); + res.status(401).json({ + jsonrpc: '2.0', + error: { + code: -32001, + message: 'Unauthorized' + }, + id: null + }); + return; + } + + // Check if Authorization header has Bearer prefix + if (!authHeader.startsWith('Bearer ')) { + logger.warn('Authentication failed: Invalid Authorization header format (expected Bearer token)', { + ip: req.ip, + userAgent: req.get('user-agent'), + reason: 'invalid_auth_format', + headerPrefix: authHeader.substring(0, 10) + '...' // Log first 10 chars for debugging + }); + res.status(401).json({ + jsonrpc: '2.0', + error: { + code: -32001, + message: 'Unauthorized' + }, + id: null + }); + return; + } + + // Extract token and trim whitespace + const token = authHeader.slice(7).trim(); + + // Check if token matches + if (token !== this.authToken) { + logger.warn('Authentication failed: Invalid token', { + ip: req.ip, + userAgent: req.get('user-agent'), + reason: 'invalid_token' }); res.status(401).json({ jsonrpc: '2.0', diff --git a/src/http-server.ts b/src/http-server.ts index 471a6ad..5c362a8 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -53,14 +53,17 @@ function validateEnvironment() { // Load auth token from env var or file authToken = loadAuthToken(); - if (!authToken) { - logger.error('No authentication token found'); - console.error('ERROR: AUTH_TOKEN is required for HTTP mode'); + if (!authToken || authToken.trim() === '') { + logger.error('No authentication token found or token is empty'); + console.error('ERROR: AUTH_TOKEN is required for HTTP mode and cannot be empty'); console.error('Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token'); console.error('Generate AUTH_TOKEN with: openssl rand -base64 32'); process.exit(1); } + // Update authToken to trimmed version + authToken = authToken.trim(); + if (authToken.length < 32) { logger.warn('AUTH_TOKEN should be at least 32 characters for security'); console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security'); @@ -182,16 +185,55 @@ export async function startFixedHTTPServer() { app.post('/mcp', async (req: express.Request, res: express.Response): Promise => { const startTime = Date.now(); - // Simple auth check + // Enhanced authentication check with specific logging const authHeader = req.headers.authorization; - const token = authHeader?.startsWith('Bearer ') - ? authHeader.slice(7) - : authHeader; - if (token !== authToken) { - logger.warn('Authentication failed', { + // Check if Authorization header is missing + if (!authHeader) { + logger.warn('Authentication failed: Missing Authorization header', { ip: req.ip, - userAgent: req.get('user-agent') + userAgent: req.get('user-agent'), + reason: 'no_auth_header' + }); + res.status(401).json({ + jsonrpc: '2.0', + error: { + code: -32001, + message: 'Unauthorized' + }, + id: null + }); + return; + } + + // Check if Authorization header has Bearer prefix + if (!authHeader.startsWith('Bearer ')) { + logger.warn('Authentication failed: Invalid Authorization header format (expected Bearer token)', { + ip: req.ip, + userAgent: req.get('user-agent'), + reason: 'invalid_auth_format', + headerPrefix: authHeader.substring(0, 10) + '...' // Log first 10 chars for debugging + }); + res.status(401).json({ + jsonrpc: '2.0', + error: { + code: -32001, + message: 'Unauthorized' + }, + id: null + }); + return; + } + + // Extract token and trim whitespace + const token = authHeader.slice(7).trim(); + + // Check if token matches + if (token !== authToken) { + logger.warn('Authentication failed: Invalid token', { + ip: req.ip, + userAgent: req.get('user-agent'), + reason: 'invalid_token' }); res.status(401).json({ jsonrpc: '2.0',