mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 17:31:33 +08:00
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:
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
Reference in New Issue
Block a user