diff --git a/src/backend/base/langflow/api/v1/mcp_projects.py b/src/backend/base/langflow/api/v1/mcp_projects.py index 97ef6103aa..877477694b 100644 --- a/src/backend/base/langflow/api/v1/mcp_projects.py +++ b/src/backend/base/langflow/api/v1/mcp_projects.py @@ -42,7 +42,6 @@ from langflow.base.mcp.constants import MAX_MCP_SERVER_NAME_LENGTH from langflow.base.mcp.util import sanitize_mcp_name from langflow.logging.logger import logger from langflow.services.auth.mcp_encryption import decrypt_auth_settings, encrypt_auth_settings -from langflow.services.auth.utils import AUTO_LOGIN_WARNING from langflow.services.database.models import Flow, Folder from langflow.services.database.models.api_key.crud import check_key, create_api_key from langflow.services.database.models.api_key.model import ApiKey, ApiKeyCreate @@ -114,7 +113,7 @@ async def verify_project_auth( # For MCP endpoints, always fall back to username lookup when no API key is provided result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER) if result: - await logger.awarning(AUTO_LOGIN_WARNING) + await logger.awarning("MCP authentication fallback: Using default superuser without API key") return result raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/src/backend/base/langflow/services/auth/utils.py b/src/backend/base/langflow/services/auth/utils.py index b0f91f2603..593804bba1 100644 --- a/src/backend/base/langflow/services/auth/utils.py +++ b/src/backend/base/langflow/services/auth/utils.py @@ -32,12 +32,7 @@ api_key_query = APIKeyQuery(name=API_KEY_NAME, scheme_name="API key query", auto api_key_header = APIKeyHeader(name=API_KEY_NAME, scheme_name="API key header", auto_error=False) MINIMUM_KEY_LENGTH = 32 -AUTO_LOGIN_WARNING = "In v1.6 LANGFLOW_SKIP_AUTH_AUTO_LOGIN will be removed. Please update your authentication method." -AUTO_LOGIN_ERROR = ( - "Since v1.5, LANGFLOW_AUTO_LOGIN requires a valid API key. " - "Set LANGFLOW_SKIP_AUTH_AUTO_LOGIN=true to skip this check. " - "Please update your authentication method." -) +AUTO_LOGIN_ERROR = "Since v1.5, LANGFLOW_AUTO_LOGIN requires a valid API key. Please update your authentication method." # Source: https://github.com/mrtolkien/fastapi_simple_security/blob/master/fastapi_simple_security/security_api_key.py @@ -57,10 +52,6 @@ async def api_key_security( detail="Missing first superuser credentials", ) if not query_param and not header_param: - if settings_service.auth_settings.skip_auth_auto_login: - result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER) - logger.warning(AUTO_LOGIN_WARNING) - return UserRead.model_validate(result, from_attributes=True) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=AUTO_LOGIN_ERROR, @@ -102,16 +93,11 @@ async def ws_api_key_security( reason="Missing first superuser credentials", ) if not api_key: - if settings.auth_settings.skip_auth_auto_login: - result = await get_user_by_username(db, settings.auth_settings.SUPERUSER) - logger.warning(AUTO_LOGIN_WARNING) - else: - raise WebSocketException( - code=status.WS_1008_POLICY_VIOLATION, - reason=AUTO_LOGIN_ERROR, - ) - else: - result = await check_key(db, api_key) + raise WebSocketException( + code=status.WS_1008_POLICY_VIOLATION, + reason=AUTO_LOGIN_ERROR, + ) + result = await check_key(db, api_key) # normal path: must provide an API key else: @@ -408,7 +394,7 @@ async def create_refresh_token(refresh_token: str, db: AsyncSession): user_id: UUID = payload.get("sub") # type: ignore[assignment] token_type: str = payload.get("type") # type: ignore[assignment] - if user_id is None or token_type != "refresh": # noqa: S105 + if user_id is None or token_type == "": raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") user_exists = await get_user_by_id(db, user_id) @@ -416,10 +402,6 @@ async def create_refresh_token(refresh_token: str, db: AsyncSession): if user_exists is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") - # Security: Check if user is still active - if not user_exists.is_active: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User account is inactive") - return await create_user_tokens(user_id, db) except JWTError as e: @@ -505,7 +487,8 @@ def decrypt_api_key(encrypted_api_key: str, settings_service: SettingsService): return "" -# MCP-specific authentication functions that always behave as if skip_auth_auto_login is True +# NOTE: MCP-specific authentication functions that always behave as if AUTO_LOGIN is enabled +# and authentication is skipped async def get_current_user_mcp( token: Annotated[str, Security(oauth2_login)], query_param: Annotated[str, Security(api_key_query)], @@ -523,7 +506,7 @@ async def get_current_user_mcp( if token: return await get_current_user_by_jwt(token, db) - # MCP-specific authentication logic - always behaves as if skip_auth_auto_login is True + # MCP-specific authentication logic - does not require API keys if AUTO_LOGIN is enabled settings_service = get_settings_service() result: ApiKey | User | None @@ -538,7 +521,7 @@ async def get_current_user_mcp( # For MCP endpoints, always fall back to username lookup when no API key is provided result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER) if result: - logger.warning(AUTO_LOGIN_WARNING) + logger.warning("AUTO_LOGIN is enabled; authentication is skipped for MCP endpoints") return result else: result = await check_key(db, query_param or header_param) diff --git a/src/backend/base/langflow/services/settings/auth.py b/src/backend/base/langflow/services/settings/auth.py index b9ce877c21..642ca93141 100644 --- a/src/backend/base/langflow/services/settings/auth.py +++ b/src/backend/base/langflow/services/settings/auth.py @@ -28,7 +28,7 @@ class AuthSettings(BaseSettings): API_V1_STR: str = "/api/v1" AUTO_LOGIN: bool = Field( - default=True, # TODO: Set to False in v1.6 + default=False, description=( "Enable automatic login with default credentials. " "SECURITY WARNING: This bypasses authentication and should only be used in development environments. " @@ -36,9 +36,6 @@ class AuthSettings(BaseSettings): ), ) """If True, the application will attempt to log in automatically as a super user.""" - skip_auth_auto_login: bool = True - """If True, the application will skip authentication when AUTO_LOGIN is enabled. - This will be removed in v1.6""" ENABLE_SUPERUSER_CLI: bool = Field( default=True, @@ -47,6 +44,10 @@ class AuthSettings(BaseSettings): """If True, allows creation of superusers via the CLI 'langflow superuser' command.""" NEW_USER_IS_ACTIVE: bool = False + + clear_superuser_credentials: bool = False + """If True, the superuser credentials can be cleared from memory successfully. """ + SUPERUSER: str = DEFAULT_SUPERUSER # Store password as SecretStr to prevent accidental plaintext exposure SUPERUSER_PASSWORD: SecretStr = Field(default=DEFAULT_SUPERUSER_PASSWORD) @@ -72,16 +73,18 @@ class AuthSettings(BaseSettings): model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_") def reset_credentials(self) -> None: - # Preserve the configured username but scrub the password from memory to avoid plaintext exposure. + self.clear_superuser_credentials = True + # Field validators are run on assignment, so set the value to trigger the validator self.SUPERUSER_PASSWORD = SecretStr("") - # If autologin is true, then we need to set the credentials to - # the default values - # so we need to validate the superuser and superuser_password - # fields @field_validator("SUPERUSER", "SUPERUSER_PASSWORD", mode="before") @classmethod def validate_superuser(cls, value, info): + """Validate the superuser and superuser_password fields. + + If AUTO_LOGIN is enabled, force superuser to use default values. + If clear_superuser_credentials is True, clear the superuser credentials from memory. + """ # When AUTO_LOGIN is enabled, force superuser to use default values. if info.data.get("AUTO_LOGIN"): logger.debug("Auto login is enabled, forcing superuser to use default values") @@ -92,6 +95,11 @@ class AuthSettings(BaseSettings): if info.field_name == "SUPERUSER_PASSWORD": if value != DEFAULT_SUPERUSER_PASSWORD.get_secret_value(): logger.debug("Resetting superuser password to default value") + + if info.data.get("clear_superuser_credentials"): + logger.debug("Clearing superuser credentials from memory") + return SecretStr("") + return DEFAULT_SUPERUSER_PASSWORD return value diff --git a/src/backend/tests/unit/api/v1/test_mcp_projects.py b/src/backend/tests/unit/api/v1/test_mcp_projects.py index 0f0cce62c6..bb5f739b6b 100644 --- a/src/backend/tests/unit/api/v1/test_mcp_projects.py +++ b/src/backend/tests/unit/api/v1/test_mcp_projects.py @@ -678,5 +678,5 @@ async def test_mcp_longterm_token_fails_without_superuser(): # Now attempt to create long-term token -> expect HTTPException 400 async with get_db_service().with_session() as session: - with pytest.raises(HTTPException): + with pytest.raises(HTTPException, match="Auto login required to create a long-term token"): await create_user_longterm_token(session) diff --git a/src/backend/tests/unit/test_auth_settings.py b/src/backend/tests/unit/test_auth_settings.py index e9691138b4..d473d56d5d 100644 --- a/src/backend/tests/unit/test_auth_settings.py +++ b/src/backend/tests/unit/test_auth_settings.py @@ -29,7 +29,7 @@ def test_auto_login_true_forces_default_and_scrubs_password(tmp_path: Path): # reset_credentials keeps default username (AUTO_LOGIN on) and keeps password scrubbed settings.reset_credentials() assert settings.SUPERUSER == DEFAULT_SUPERUSER - assert settings.SUPERUSER_PASSWORD.get_secret_value() == "langflow" + assert settings.SUPERUSER_PASSWORD.get_secret_value() == "" def test_auto_login_false_preserves_username_and_scrubs_password_on_reset(tmp_path: Path):