- 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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')\"",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user