diff --git a/backend/main.py b/backend/main.py index 26b04c9..1b7dc9d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -110,17 +110,46 @@ def check_docker_availability(): # ✅ ENHANCED: Flask app configuration with your .env variables app = Flask(__name__) + +class MissingSecretError(ValueError): + """Raised when a required secret is not set in environment variables.""" + pass + def get_required_secret(env_var: str, description: str) -> str: - """Get required secret from environment, raise error if not set""" + """ + Get required secret from environment. + + Args: + env_var: Name of the environment variable + description: Human-readable description of the secret + + Returns: + The secret value from the environment + + Raises: + MissingSecretError: If the environment variable is not set + """ value = os.getenv(env_var) if not value: - raise ValueError(f"{description} ({env_var}) must be set in environment variables for security. Do not use default values for secrets.") + raise MissingSecretError(f"{description} ({env_var}) must be set in environment variables for security. Do not use default values for secrets.") return value def get_dev_fallback_secret(name: str) -> str: """ Generate a persistent random secret for development use only. - Stores the secret in a file to persist across restarts. + + Stores the secret in a file in the system temp directory to persist across restarts. + Files are created with restrictive permissions (0600) to limit access. + + Args: + name: Unique identifier for this secret (used in filename) + + Returns: + A 64-character hex string (32 bytes of randomness) + + Security Note: + These secrets are stored in temp files and should only be used for development. + In production, always set proper secrets via environment variables. """ import tempfile import stat @@ -147,7 +176,7 @@ try: _secret_key = get_required_secret('SECRET_KEY', 'Flask secret key') _jwt_secret_key = get_required_secret('JWT_SECRET_KEY', 'JWT secret key') _admin_token = get_required_secret('ADMIN_TOKEN', 'Admin authentication token') -except ValueError as e: +except MissingSecretError as e: print(f"⚠️ SECURITY WARNING: {e}") print("⚠️ Using persistent development secrets. Set proper secrets in production!") _secret_key = os.getenv('SECRET_KEY') or get_dev_fallback_secret('secret_key') diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 4d41d65..957157c 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -22,7 +22,13 @@ if not JWT_SECRET: import warnings import tempfile import stat + import secrets as secrets_module warnings.warn("JWT_SECRET environment variable not set. Using persistent dev secret.", UserWarning) + + def _generate_and_store_secret(): + """Generate a random secret and store it with restrictive permissions.""" + return secrets_module.token_hex(32) + # Use persistent file-based secret for development to avoid invalidating tokens on restart _secret_file = os.path.join(tempfile.gettempdir(), '.openlearnx_dev_jwt_secret_auth') try: @@ -30,15 +36,13 @@ if not JWT_SECRET: with open(_secret_file, 'r') as f: JWT_SECRET = f.read().strip() if not JWT_SECRET: - import secrets as _secrets - JWT_SECRET = _secrets.token_hex(32) + JWT_SECRET = _generate_and_store_secret() with open(_secret_file, 'w') as f: f.write(JWT_SECRET) # Set restrictive permissions (owner read/write only) os.chmod(_secret_file, stat.S_IRUSR | stat.S_IWUSR) except Exception: - import secrets as _secrets - JWT_SECRET = _secrets.token_hex(32) + JWT_SECRET = _generate_and_store_secret() @bp.route('/nonce', methods=['POST', 'OPTIONS']) def get_nonce():