Configuration Validation
Status: ✅ IMPLEMENTED Priority: 🔴 HIGH Implementation Time: 6-8 hours Risk Level: LOW Impact: Prevent runtime failures from invalid configuration
Implementation Summary
Configuration validation has been successfully implemented at startup:
- ✅ Comprehensive validation module created (src/config/validation.ts)
- ✅ Server configuration validated (port ranges, URL formats)
- ✅ Authentication configuration validated (OIDC issuer, audience, OAuth endpoints)
- ✅ Token configuration validated (TTL, refresh buffer, JWKS cache)
- ✅ Rate limiting validated (window size, max requests, global limits)
- ✅ Redis URL format validated (redis:// and rediss:// protocols)
- ✅ Session and DCR configuration validated
- ✅ Production-specific security requirements enforced
- ✅ Clear error messages with exit code 1 on failure
- ✅ Warning messages for suboptimal configurations
- ✅ Comprehensive test coverage (42 tests in src/config/validation.test.ts)
Implementation Date: 2026-01-06
Problem Statement (Original)
The server currently starts with invalid or missing configuration, only discovering errors when features are used. This leads to:
Examples of Silent Failures:
bash
# These all "succeed" at startup but fail at runtime
PORT=-1 # Invalid port (crashes on bind)
OIDC_ISSUER=not-a-url # Invalid URL (crashes on first auth)
TOKEN_REFRESH_BUFFER_SECONDS=-300 # Negative value (breaks refresh logic)
MCP_RATE_LIMIT_MAX=999999999999 # Unrealistic value (OOM risk)
REDIS_URL=malformed # Invalid URL (crashes on first Redis op)
AUTH_REQUIRED=false # in production # Security riskImpact:
- Production deployments fail after startup
- Errors discovered during user requests
- Poor developer experience
- Security misconfigurations deployed
Current Behavior
vs. Desired Behavior:
Proposed Solution
Implement comprehensive configuration validation at startup with clear error messages and production-specific checks.
Implementation
1. Configuration Validation Module
Create src/config/validation.ts:
typescript
import { Config } from "./index.js";
import { logger } from "../services/logger.js";
interface ValidationError {
field: string;
message: string;
severity: "error" | "warning";
}
export class ConfigValidator {
private errors: ValidationError[] = [];
private warnings: ValidationError[] = [];
/**
* Validate complete configuration
*/
validate(config: Config): void {
// Server configuration
this.validateServer(config);
// Authentication configuration
if (config.authRequired) {
this.validateAuth(config);
}
// Token refresh configuration
if (config.tokens.autoRefreshEnabled) {
this.validateTokenRefresh(config);
}
// Rate limiting configuration
this.validateRateLimiting(config);
// Redis configuration
this.validateRedis(config);
// Production-specific validation
if (config.nodeEnv === "production") {
this.validateProduction(config);
}
// Report results
this.reportResults();
}
/**
* Validate server configuration
*/
private validateServer(config: Config): void {
// Port validation
if (config.port < 1 || config.port > 65535) {
this.addError("PORT", `Invalid port number: ${config.port} (must be 1-65535)`);
}
if (config.port < 1024) {
this.addWarning("PORT", `Port ${config.port} is privileged (< 1024), may require root/CAP_NET_BIND_SERVICE`);
}
// Base URL validation
if (config.baseUrl) {
if (!this.isValidUrl(config.baseUrl)) {
this.addError("BASE_URL", `Invalid BASE_URL format: ${config.baseUrl}`);
}
if (!config.baseUrl.startsWith("https://") && config.nodeEnv === "production") {
this.addWarning("BASE_URL", "BASE_URL should use HTTPS in production");
}
} else if (config.authRequired) {
this.addWarning("BASE_URL", "BASE_URL not set (needed for OAuth metadata and WWW-Authenticate header)");
}
// Node environment
if (!["development", "production", "test"].includes(config.nodeEnv)) {
this.addWarning("NODE_ENV", `Unexpected NODE_ENV value: ${config.nodeEnv}`);
}
}
/**
* Validate authentication configuration
*/
private validateAuth(config: Config): void {
// OIDC Issuer
if (!config.oidc.issuer) {
this.addError("OIDC_ISSUER", "OIDC_ISSUER is required when AUTH_REQUIRED=true");
} else if (!this.isValidUrl(config.oidc.issuer)) {
this.addError("OIDC_ISSUER", `Invalid OIDC_ISSUER format: ${config.oidc.issuer}`);
} else if (!config.oidc.issuer.startsWith("https://") && config.nodeEnv === "production") {
this.addWarning("OIDC_ISSUER", "OIDC_ISSUER should use HTTPS in production");
}
// OIDC Audience
if (!config.oidc.audience) {
this.addError("OIDC_AUDIENCE", "OIDC_AUDIENCE is required when AUTH_REQUIRED=true");
} else if (config.oidc.audience.trim().length === 0) {
this.addError("OIDC_AUDIENCE", "OIDC_AUDIENCE cannot be empty");
}
// JWKS URL (optional, but validate if provided)
if (config.oidc.jwksUrl && !this.isValidUrl(config.oidc.jwksUrl)) {
this.addError("OIDC_JWKS_URL", `Invalid OIDC_JWKS_URL format: ${config.oidc.jwksUrl}`);
}
// OAuth Token URL
if (!config.oauth.tokenUrl) {
this.addError("OAUTH_TOKEN_URL", "OAUTH_TOKEN_URL is required when AUTH_REQUIRED=true");
} else if (!this.isValidUrl(config.oauth.tokenUrl)) {
this.addError("OAUTH_TOKEN_URL", `Invalid OAUTH_TOKEN_URL format: ${config.oauth.tokenUrl}`);
}
// OAuth Authorization URL
if (!config.oauth.authorizationUrl) {
this.addError("OAUTH_AUTHORIZATION_URL", "OAUTH_AUTHORIZATION_URL is required when AUTH_REQUIRED=true");
} else if (!this.isValidUrl(config.oauth.authorizationUrl)) {
this.addError("OAUTH_AUTHORIZATION_URL", `Invalid OAUTH_AUTHORIZATION_URL format: ${config.oauth.authorizationUrl}`);
}
// OAuth Revocation URL (optional)
if (config.oauth.revocationUrl && !this.isValidUrl(config.oauth.revocationUrl)) {
this.addError("OAUTH_REVOCATION_URL", `Invalid OAUTH_REVOCATION_URL format: ${config.oauth.revocationUrl}`);
}
// JWKS cache TTL
if (config.oidc.jwks.cacheTtlMs < 60000) {
this.addWarning("OIDC_JWKS_CACHE_TTL", `JWKS cache TTL is very low: ${config.oidc.jwks.cacheTtlMs}ms (< 1 minute)`);
}
if (config.oidc.jwks.cacheTtlMs > 86400000) {
this.addWarning("OIDC_JWKS_CACHE_TTL", `JWKS cache TTL is very high: ${config.oidc.jwks.cacheTtlMs}ms (> 1 day)`);
}
}
/**
* Validate token refresh configuration
*/
private validateTokenRefresh(config: Config): void {
// Refresh buffer
if (config.tokens.refreshBufferSeconds < 0) {
this.addError("TOKEN_REFRESH_BUFFER_SECONDS", `Negative value not allowed: ${config.tokens.refreshBufferSeconds}`);
}
if (config.tokens.refreshBufferSeconds === 0) {
this.addWarning("TOKEN_REFRESH_BUFFER_SECONDS", "Refresh buffer is 0 - tokens will only refresh after expiration (reactive only)");
}
if (config.tokens.refreshBufferSeconds > 3600) {
this.addWarning("TOKEN_REFRESH_BUFFER_SECONDS", `Refresh buffer is very high: ${config.tokens.refreshBufferSeconds}s (> 1 hour)`);
}
// Token store TTL
if (config.tokens.storeTtlSeconds < 3600) {
this.addWarning("TOKEN_STORE_TTL", `Token store TTL is very low: ${config.tokens.storeTtlSeconds}s (< 1 hour)`);
}
if (config.tokens.storeTtlSeconds > 604800) {
this.addWarning("TOKEN_STORE_TTL", `Token store TTL is very high: ${config.tokens.storeTtlSeconds}s (> 7 days)`);
}
// Pending token TTL
if (config.tokens.pendingTtlSeconds < 60) {
this.addWarning("TOKEN_PENDING_TTL", `Pending token TTL is very low: ${config.tokens.pendingTtlSeconds}s (< 1 minute)`);
}
if (config.tokens.pendingTtlSeconds > 600) {
this.addWarning("TOKEN_PENDING_TTL", `Pending token TTL is very high: ${config.tokens.pendingTtlSeconds}s (> 10 minutes)`);
}
}
/**
* Validate rate limiting configuration
*/
private validateRateLimiting(config: Config): void {
// MCP rate limit
if (config.rateLimit.mcp.max < 1) {
this.addError("MCP_RATE_LIMIT_MAX", `Invalid value: ${config.rateLimit.mcp.max} (must be > 0)`);
}
if (config.rateLimit.mcp.max > 100000) {
this.addWarning("MCP_RATE_LIMIT_MAX", `Very high limit: ${config.rateLimit.mcp.max} (> 100k requests per window)`);
}
if (config.rateLimit.mcp.windowMs < 1000) {
this.addWarning("MCP_RATE_LIMIT_WINDOW_MS", `Very short window: ${config.rateLimit.mcp.windowMs}ms (< 1 second)`);
}
// DCR rate limit
if (config.dcr.rateLimit.maxRequests < 1) {
this.addError("DCR_RATE_LIMIT_MAX", `Invalid value: ${config.dcr.rateLimit.maxRequests} (must be > 0)`);
}
// Global rate limits
if (config.rateLimit.mcp.globalMax && config.rateLimit.mcp.globalMax < config.rateLimit.mcp.max) {
this.addWarning("MCP_GLOBAL_RATE_LIMIT_MAX", "Global rate limit should be higher than per-IP limit");
}
}
/**
* Validate Redis configuration
*/
private validateRedis(config: Config): void {
if (!this.isValidRedisUrl(config.dcr.redisUrl)) {
this.addError("REDIS_URL", `Invalid Redis URL format: ${config.dcr.redisUrl}`);
}
// Check for localhost in production
const hasLocalhost = config.dcr.redisUrl.includes("localhost") ||
config.dcr.redisUrl.includes("127.0.0.1");
if (hasLocalhost && config.nodeEnv === "production") {
this.addWarning("REDIS_URL", "Using localhost Redis in production (not scalable)");
}
// Check for password in production
const hasPassword = config.dcr.redisUrl.includes(":");
const urlParts = config.dcr.redisUrl.split("@");
if (!hasPassword && config.nodeEnv === "production" && urlParts.length === 1) {
this.addWarning("REDIS_URL", "Redis URL has no password in production (security risk)");
}
// Check for TLS in production
if (!config.dcr.redisUrl.startsWith("rediss://") && config.nodeEnv === "production") {
this.addWarning("REDIS_URL", "Redis URL not using TLS (rediss://) in production");
}
}
/**
* Validate production-specific requirements
*/
private validateProduction(config: Config): void {
// Authentication must be enabled
if (!config.authRequired) {
this.addError("AUTH_REQUIRED", "❌ CRITICAL: AUTH_REQUIRED=false is not allowed in production");
}
// Metrics should be enabled
if (!config.metrics.enabled) {
this.addWarning("METRICS_ENABLED", "Metrics disabled in production (monitoring blind spot)");
}
// Session TTL reasonable
if (config.session.ttlSeconds < 3600) {
this.addWarning("MCP_SESSION_TTL_SECONDS", `Short session TTL in production: ${config.session.ttlSeconds}s (< 1 hour)`);
}
// DCR client TTL reasonable
if (config.dcr.clientTtlSeconds < 86400) {
this.addWarning("DCR_CLIENT_TTL", `Short client TTL in production: ${config.dcr.clientTtlSeconds}s (< 1 day)`);
}
// Check environment variable pollution
if (process.env.AUTH_REQUIRED === "false") {
this.addError("AUTH_REQUIRED", "AUTH_REQUIRED explicitly set to false in production environment");
}
}
/**
* Add validation error
*/
private addError(field: string, message: string): void {
this.errors.push({ field, message, severity: "error" });
}
/**
* Add validation warning
*/
private addWarning(field: string, message: string): void {
this.warnings.push({ field, message, severity: "warning" });
}
/**
* Report validation results
*/
private reportResults(): void {
if (this.warnings.length > 0) {
console.warn("\n⚠️ Configuration Warnings:");
for (const warning of this.warnings) {
console.warn(` [${warning.field}] ${warning.message}`);
}
}
if (this.errors.length > 0) {
console.error("\n❌ Configuration Validation Failed:");
for (const error of this.errors) {
console.error(` [${error.field}] ${error.message}`);
}
console.error("\nServer cannot start with invalid configuration.");
console.error("Please fix the above errors and try again.\n");
process.exit(1);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
logger.info("✅ Configuration validation passed");
}
}
/**
* Check if string is valid URL
*/
private isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
/**
* Check if string is valid Redis URL
*/
private isValidRedisUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === "redis:" || url.protocol === "rediss:";
} catch {
return false;
}
}
}
/**
* Validate configuration and exit if invalid
*/
export function validateConfig(config: Config): void {
const validator = new ConfigValidator();
validator.validate(config);
}2. Integrate into Application Startup
Update src/index.ts:
typescript
import { app } from "./app.js";
import { config } from "./config/index.js";
import { validateConfig } from "./config/validation.js";
import { logger } from "./services/logger.js";
// Validate configuration BEFORE starting server
validateConfig(config);
// Update logger metadata with actual config values
logger.defaultMeta = {
service: config.server.name,
version: config.server.version,
};
const server = app.listen(config.port, () => {
logger.info(`Seed MCP server running on http://localhost:${String(config.port)}/mcp`);
if (!config.authRequired) {
logger.warn(
"⚠️ SECURITY WARNING: Authentication is DISABLED (AUTH_REQUIRED=false). " +
"This should only be used in development/testing environments.",
);
} else {
logger.info(`Authentication enabled, OIDC issuer: ${config.oidc.issuer || "(not configured)"}`);
}
});
// ... rest of startup codeExample Output
Valid Configuration
✅ Configuration validation passed
Seed MCP server running on http://localhost:3000/mcp
Authentication enabled, OIDC issuer: https://auth.example.com/application/o/seed/Configuration with Warnings
⚠️ Configuration Warnings:
[BASE_URL] BASE_URL not set (needed for OAuth metadata and WWW-Authenticate header)
[REDIS_URL] Using localhost Redis in production (not scalable)
[TOKEN_REFRESH_BUFFER_SECONDS] Refresh buffer is very high: 3600s (> 1 hour)
Seed MCP server running on http://localhost:3000/mcp
Authentication enabled, OIDC issuer: https://auth.example.com/application/o/seed/Invalid Configuration
⚠️ Configuration Warnings:
[PORT] Port 80 is privileged (< 1024), may require root/CAP_NET_BIND_SERVICE
[BASE_URL] BASE_URL should use HTTPS in production
❌ Configuration Validation Failed:
[AUTH_REQUIRED] ❌ CRITICAL: AUTH_REQUIRED=false is not allowed in production
[OIDC_ISSUER] Invalid OIDC_ISSUER format: not-a-url
[PORT] Invalid port number: -1 (must be 1-65535)
[TOKEN_REFRESH_BUFFER_SECONDS] Negative value not allowed: -300
Server cannot start with invalid configuration.
Please fix the above errors and try again.
Process exited with code 1Testing
Unit Tests
typescript
describe('Configuration Validation', () => {
it('should pass with valid production config', () => {
const config = {
port: 3000,
baseUrl: 'https://seed.example.com',
authRequired: true,
nodeEnv: 'production',
oidc: {
issuer: 'https://auth.example.com',
audience: 'seed-client',
jwksUrl: '',
jwks: { cacheTtlMs: 3600000, refreshBeforeExpiryMs: 300000 },
},
// ... rest of config
};
expect(() => validateConfig(config)).not.toThrow();
});
it('should fail with invalid port', () => {
const config = { ...validConfig, port: -1 };
expect(() => validateConfig(config)).toThrow();
});
it('should fail with AUTH_REQUIRED=false in production', () => {
const config = {
...validConfig,
nodeEnv: 'production',
authRequired: false,
};
expect(() => validateConfig(config)).toThrow();
});
it('should warn about localhost Redis in production', () => {
const consoleWarn = jest.spyOn(console, 'warn');
const config = {
...validConfig,
nodeEnv: 'production',
dcr: { redisUrl: 'redis://localhost:6379' },
};
validateConfig(config);
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining('Using localhost Redis in production')
);
});
});Acceptance Criteria
- [ ] Validates all configuration values at startup
- [ ] Checks data types and ranges
- [ ] Validates URL formats
- [ ] Production-specific validation
- [ ] Clear error messages with field names
- [ ] Warnings for sub-optimal but valid configs
- [ ] Exits with code 1 on validation failure
- [ ] Colored console output (errors red, warnings yellow)
- [ ] Does not start server with invalid config
- [ ] Unit tests for all validation rules
- [ ] Documentation of all validated fields
Configuration Checklist
Development
- [ ]
AUTH_REQUIRED=falseallowed - [ ] HTTP URLs allowed
- [ ] Localhost Redis allowed
- [ ] Short TTLs allowed
- [ ] No password required
Production
- [ ]
AUTH_REQUIRED=trueenforced - [ ] HTTPS URLs required for public endpoints
- [ ] TLS Redis recommended (rediss://)
- [ ] Redis password recommended
- [ ] Reasonable TTL values
- [ ]
BASE_URLset and valid - [ ] Metrics enabled
Future Enhancements
Runtime Configuration Reload
- Watch for config changes
- Validate before applying
- Hot-reload supported values
- Restart required for critical changes
Configuration Schema
- JSON Schema for validation
- Auto-generate TypeScript types
- IDE autocomplete support
- Schema documentation generation
Environment Variable Discovery
- Detect typos in env var names
- Suggest correct names for common mistakes
- Warn about unused environment variables
Related Enhancements
- Graceful Shutdown - Shutdown validation flag
- Health Check Improvements - Configuration health checks