feat: enhanced authentication logging for better debugging (fixes #22, #16)

- Added specific error reasons for auth failures: no_auth_header, invalid_auth_format, invalid_token
- Fixed AUTH_TOKEN_FILE support in Docker production stacks (issue #16)
- Added AUTH_TOKEN_FILE support to single-session HTTP server for consistency
- Enhanced security by removing token lengths from logs
- Added token trimming and empty token validation
- Updated Docker entrypoint to properly support AUTH_TOKEN_FILE
- Bumped version to 2.7.10

This improves debugging for mcp-remote authentication issues and enables
proper Docker secrets usage in production environments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-07 23:19:35 +02:00
parent 6f11d339ca
commit 87f0cfc4dc
8 changed files with 169 additions and 29 deletions

View File

@@ -566,7 +566,7 @@ For detailed deployment instructions, see [HTTP Deployment Guide](./docs/HTTP_DE
**Technical Details**: **Technical Details**:
- `src/http-server-single-session.ts` - Single-session implementation (partial fix) - `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 - `src/utils/console-manager.ts` - Console output isolation
- Use `USE_FIXED_HTTP=true` to enable the fixed implementation - Use `USE_FIXED_HTTP=true` to enable the fixed implementation

Binary file not shown.

View File

@@ -2,8 +2,14 @@
set -e set -e
# Environment variable validation # Environment variable validation
if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ]; then if [ "$MCP_MODE" = "http" ] && [ -z "$AUTH_TOKEN" ] && [ -z "$AUTH_TOKEN_FILE" ]; then
echo "ERROR: AUTH_TOKEN is required for HTTP mode" 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 exit 1
fi fi

View File

@@ -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/), 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). 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 ## [2.7.8] - 2025-07-06
### Added ### 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 ## [2.7.5] - 2025-07-06
### Added ### 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 - Known Issues section in README documenting Claude Desktop container duplication bug
- Enhanced authentication documentation in Docker README - Enhanced authentication documentation in Docker README
### Fixed ### 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 - HTTP server now properly supports both AUTH_TOKEN and AUTH_TOKEN_FILE environment variables
### Changed ### Changed
@@ -344,6 +363,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Basic n8n and MCP integration - Basic n8n and MCP integration
- Core workflow automation features - 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.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.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 [2.7.4]: https://github.com/czlonkowski/n8n-mcp/compare/v2.7.3...v2.7.4

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.7.9", "version": "2.7.10",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@@ -41,6 +41,7 @@
"test:tools-documentation": "node dist/scripts/test-tools-documentation.js", "test:tools-documentation": "node dist/scripts/test-tools-documentation.js",
"test:mcp:update-partial": "node dist/scripts/test-mcp-n8n-update-partial.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: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", "sanitize:templates": "node dist/scripts/sanitize-templates.js",
"db:rebuild": "node dist/scripts/rebuild-database.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')\"", "db:init": "node -e \"new (require('./dist/services/sqlite-storage-service').SQLiteStorageService)(); console.log('Database initialized')\"",

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp-runtime", "name": "n8n-mcp-runtime",
"version": "2.7.9", "version": "2.7.10",
"description": "n8n MCP Server Runtime Dependencies Only", "description": "n8n MCP Server Runtime Dependencies Only",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

@@ -9,6 +9,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import { N8NDocumentationMCPServer } from './mcp/server'; import { N8NDocumentationMCPServer } from './mcp/server';
import { ConsoleManager } from './utils/console-manager'; import { ConsoleManager } from './utils/console-manager';
import { logger } from './utils/logger'; import { logger } from './utils/logger';
import { readFileSync } from 'fs';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
@@ -25,26 +26,57 @@ export class SingleSessionHTTPServer {
private consoleManager = new ConsoleManager(); private consoleManager = new ConsoleManager();
private expressServer: any; private expressServer: any;
private sessionTimeout = 30 * 60 * 1000; // 30 minutes private sessionTimeout = 30 * 60 * 1000; // 30 minutes
private authToken: string | null = null;
constructor() { constructor() {
// Validate environment on construction // Validate environment on construction
this.validateEnvironment(); 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 * Validate required environment variables
*/ */
private validateEnvironment(): void { private validateEnvironment(): void {
const required = ['AUTH_TOKEN']; // Load auth token from env var or file
const missing = required.filter(key => !process.env[key]); this.authToken = this.loadAuthToken();
if (missing.length > 0) { if (!this.authToken || this.authToken.trim() === '') {
const message = `Missing required environment variables: ${missing.join(', ')}`; 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); logger.error(message);
throw new 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'); logger.warn('AUTH_TOKEN should be at least 32 characters for security');
} }
} }
@@ -220,16 +252,55 @@ export class SingleSessionHTTPServer {
// Main MCP endpoint with authentication // Main MCP endpoint with authentication
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => { app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
// Simple auth check // Enhanced authentication check with specific logging
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
if (token !== process.env.AUTH_TOKEN) { // Check if Authorization header is missing
logger.warn('Authentication failed', { if (!authHeader) {
logger.warn('Authentication failed: Missing Authorization header', {
ip: req.ip, 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({ res.status(401).json({
jsonrpc: '2.0', jsonrpc: '2.0',

View File

@@ -53,14 +53,17 @@ function validateEnvironment() {
// Load auth token from env var or file // Load auth token from env var or file
authToken = loadAuthToken(); authToken = loadAuthToken();
if (!authToken) { if (!authToken || authToken.trim() === '') {
logger.error('No authentication token found'); logger.error('No authentication token found or token is empty');
console.error('ERROR: AUTH_TOKEN is required for HTTP mode'); 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('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'); console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
process.exit(1); process.exit(1);
} }
// Update authToken to trimmed version
authToken = authToken.trim();
if (authToken.length < 32) { if (authToken.length < 32) {
logger.warn('AUTH_TOKEN should be at least 32 characters for security'); 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'); 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<void> => { app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
const startTime = Date.now(); const startTime = Date.now();
// Simple auth check // Enhanced authentication check with specific logging
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
if (token !== authToken) { // Check if Authorization header is missing
logger.warn('Authentication failed', { if (!authHeader) {
logger.warn('Authentication failed: Missing Authorization header', {
ip: req.ip, 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({ res.status(401).json({
jsonrpc: '2.0', jsonrpc: '2.0',