Skip to content

Configuration Validation

Status: ✅ IMPLEMENTED Priority: 🔴 HIGH Implementation Time: 6-8 hours Risk Level: LOW Impact: Prevent runtime failures from invalid configuration

← Back to Enhancements


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 risk

Impact:

  • 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 code

Example 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 1

Testing

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=false allowed
  • [ ] HTTP URLs allowed
  • [ ] Localhost Redis allowed
  • [ ] Short TTLs allowed
  • [ ] No password required

Production

  • [ ] AUTH_REQUIRED=true enforced
  • [ ] HTTPS URLs required for public endpoints
  • [ ] TLS Redis recommended (rediss://)
  • [ ] Redis password recommended
  • [ ] Reasonable TTL values
  • [ ] BASE_URL set 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


References

Released under the MIT License.