ref: update auto login behavior to use secure defaults (#9825)

* Update auto login behavior to use secure defaults; remove skip auth option

* Add warning back

* ordering of params
This commit is contained in:
Jordan Frazier
2025-09-12 11:12:34 -04:00
committed by GitHub
parent fd88a3caab
commit 567d0fa91a
5 changed files with 31 additions and 41 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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):