Skip to content

Commit 3a3e205

Browse files
feat(auth): update AUTO_LOGIN authentication to enforce API key or JWT requirement (#8513)
* feat(auth): update AUTO_LOGIN authentication to enforce API key or JWT requirement * Removed deprecated warning messages and implemented explicit HTTP exceptions for missing API key or JWT in both API and WebSocket authentication methods. * Enhanced error handling to ensure compliance with the new authentication requirements introduced in v1.5. * fix(auth): refine error message for AUTO_LOGIN API key requirement * Updated the error message in the API key security function to clarify that AUTO_LOGIN requires a valid API key, removing the mention of JWT for consistency with the latest authentication requirements. * feat(auth): introduce SKIP_AUTH_AUTO_LOGIN setting for enhanced authentication flexibility * Added a new configuration option `SKIP_AUTH_AUTO_LOGIN` to the AuthSettings class, allowing the application to bypass API key validation for auto login. * Updated the API and WebSocket security functions to utilize this setting, improving error handling and providing a fallback for superuser credentials when authentication is skipped. * refactor(auth): rename SKIP_AUTH_AUTO_LOGIN to skip_auth_auto_login for consistency * Updated the `SKIP_AUTH_AUTO_LOGIN` setting in the `AuthSettings` class to `skip_auth_auto_login` to follow Python naming conventions. * Adjusted references in the API and WebSocket security functions to use the new attribute name, ensuring consistent behavior across the authentication logic. * feat(auth): add deprecation warning for SKIP_AUTH_AUTO_LOGIN removal * Introduced a warning log in both API and WebSocket security functions to inform users that the `LANGFLOW_SKIP_AUTH_AUTO_LOGIN` feature will be removed in version 1.6, prompting necessary updates to authentication methods. * feat(auth): enhance deprecation warnings for AUTO_LOGIN features * Added constants for deprecation warning and error messages related to `LANGFLOW_SKIP_AUTH_AUTO_LOGIN` and `AUTO_LOGIN` requirements, improving code maintainability and clarity. * Updated API and WebSocket security functions to utilize these constants for logging and exception handling, ensuring consistent messaging across authentication methods. * fix(auth): update AUTO_LOGIN_ERROR message to include LANGFLOW_SKIP_AUTH_AUTO_LOGIN usage * fix(auth): correct logic for API key validation in WebSocket security function * Adjusted the conditional flow in the `ws_api_key_security` function to ensure that the API key is checked only when necessary, improving the clarity and correctness of the authentication logic. * [autofix.ci] apply automated fixes * feat(tests): add authentication token retrieval for starter projects integration tests * Implemented a helper function to obtain a JWT token for API requests, enhancing the security of the integration tests. * Updated the test for starter projects to include the token in API requests, ensuring proper authentication during testing. * feat(auth): add MCP-specific user authentication and active user dependency * Introduced `get_current_user_mcp` function for MCP-specific user authentication, allowing fallback to username lookup when no API key is provided. * Added `get_current_active_user_mcp` dependency to manage active user checks for MCP, ensuring proper integration with the authentication flow. * refactor(api): replace user dependency with CurrentActiveMCPUser in mcp project endpoints * Updated project-related API endpoints to use CurrentActiveMCPUser for user authentication, enhancing clarity and consistency in user management. * Removed unused imports and dependencies related to the previous user authentication method, streamlining the codebase. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 6c13d91 commit 3a3e205

File tree

6 files changed

+157
-43
lines changed

6 files changed

+157
-43
lines changed

src/backend/base/langflow/api/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from sqlmodel.ext.asyncio.session import AsyncSession
1414

1515
from langflow.graph.graph.base import Graph
16-
from langflow.services.auth.utils import get_current_active_user
16+
from langflow.services.auth.utils import get_current_active_user, get_current_active_user_mcp
1717
from langflow.services.database.models.flow.model import Flow
1818
from langflow.services.database.models.message.model import MessageTable
1919
from langflow.services.database.models.transactions.model import TransactionTable
@@ -33,6 +33,7 @@
3333
MIN_PAGE_SIZE = 1
3434

3535
CurrentActiveUser = Annotated[User, Depends(get_current_active_user)]
36+
CurrentActiveMCPUser = Annotated[User, Depends(get_current_active_user_mcp)]
3637
DbSession = Annotated[AsyncSession, Depends(get_session)]
3738

3839

src/backend/base/langflow/api/v1/mcp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@
33
from collections.abc import Awaitable, Callable
44
from contextvars import ContextVar
55
from functools import wraps
6-
from typing import Annotated, Any, ParamSpec, TypeVar
6+
from typing import Any, ParamSpec, TypeVar
77
from urllib.parse import quote, unquote, urlparse
88
from uuid import uuid4
99

1010
import pydantic
1111
from anyio import BrokenResourceError
12-
from fastapi import APIRouter, Depends, HTTPException, Request, Response
12+
from fastapi import APIRouter, HTTPException, Request, Response
1313
from fastapi.responses import HTMLResponse, StreamingResponse
1414
from loguru import logger
1515
from mcp import types
1616
from mcp.server import NotificationOptions, Server
1717
from mcp.server.sse import SseServerTransport
1818
from sqlmodel import select
1919

20+
from langflow.api.utils import CurrentActiveMCPUser
2021
from langflow.api.v1.endpoints import simple_run_flow
2122
from langflow.api.v1.schemas import SimplifiedAPIRequest
2223
from langflow.base.mcp.constants import MAX_MCP_TOOL_NAME_LENGTH
2324
from langflow.base.mcp.util import get_flow_snake_case
2425
from langflow.helpers.flow import json_schema_from_flow
2526
from langflow.schema.message import Message
26-
from langflow.services.auth.utils import get_current_active_user
2727
from langflow.services.database.models.flow.model import Flow
2828
from langflow.services.database.models.user.model import User
2929
from langflow.services.deps import (
@@ -345,7 +345,7 @@ async def im_alive():
345345

346346

347347
@router.get("/sse", response_class=StreamingResponse)
348-
async def handle_sse(request: Request, current_user: Annotated[User, Depends(get_current_active_user)]):
348+
async def handle_sse(request: Request, current_user: CurrentActiveMCPUser):
349349
msg = f"Starting SSE connection, server name: {server.name}"
350350
logger.info(msg)
351351
token = current_user_ctx.set(current_user)

src/backend/base/langflow/api/v1/mcp_projects.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@
99
from datetime import datetime, timezone
1010
from ipaddress import ip_address
1111
from pathlib import Path
12-
from typing import Annotated
1312
from urllib.parse import quote, unquote, urlparse
1413
from uuid import UUID, uuid4
1514

1615
from anyio import BrokenResourceError
17-
from fastapi import APIRouter, Depends, HTTPException, Request, Response
16+
from fastapi import APIRouter, HTTPException, Request, Response
1817
from fastapi.responses import HTMLResponse
1918
from mcp import types
2019
from mcp.server import NotificationOptions, Server
2120
from mcp.server.sse import SseServerTransport
2221
from sqlalchemy.orm import selectinload
2322
from sqlmodel import select
2423

24+
from langflow.api.utils import CurrentActiveMCPUser
2525
from langflow.api.v1.endpoints import simple_run_flow
2626
from langflow.api.v1.mcp import (
2727
current_user_ctx,
@@ -34,8 +34,7 @@
3434
from langflow.base.mcp.util import get_flow_snake_case, get_unique_name
3535
from langflow.helpers.flow import json_schema_from_flow
3636
from langflow.schema.message import Message
37-
from langflow.services.auth.utils import get_current_active_user
38-
from langflow.services.database.models import Flow, Folder, User
37+
from langflow.services.database.models import Flow, Folder
3938
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME, NEW_FOLDER_NAME
4039
from langflow.services.deps import get_settings_service, get_storage_service, session_scope
4140
from langflow.services.storage.utils import build_content_type_from_extension
@@ -62,7 +61,7 @@ def get_project_sse(project_id: UUID) -> SseServerTransport:
6261
@router.get("/{project_id}")
6362
async def list_project_tools(
6463
project_id: UUID,
65-
current_user: Annotated[User, Depends(get_current_active_user)],
64+
current_user: CurrentActiveMCPUser,
6665
*,
6766
mcp_enabled: bool = True,
6867
) -> list[MCPSettings]:
@@ -136,7 +135,7 @@ async def im_alive():
136135
async def handle_project_sse(
137136
project_id: UUID,
138137
request: Request,
139-
current_user: Annotated[User, Depends(get_current_active_user)],
138+
current_user: CurrentActiveMCPUser,
140139
):
141140
"""Handle SSE connections for a specific project."""
142141
# Verify project exists and user has access
@@ -187,9 +186,7 @@ async def handle_project_sse(
187186

188187

189188
@router.post("/{project_id}")
190-
async def handle_project_messages(
191-
project_id: UUID, request: Request, current_user: Annotated[User, Depends(get_current_active_user)]
192-
):
189+
async def handle_project_messages(project_id: UUID, request: Request, current_user: CurrentActiveMCPUser):
193190
"""Handle POST messages for a project-specific MCP server."""
194191
# Verify project exists and user has access
195192
async with session_scope() as session:
@@ -216,9 +213,7 @@ async def handle_project_messages(
216213

217214

218215
@router.post("/{project_id}/")
219-
async def handle_project_messages_with_slash(
220-
project_id: UUID, request: Request, current_user: Annotated[User, Depends(get_current_active_user)]
221-
):
216+
async def handle_project_messages_with_slash(project_id: UUID, request: Request, current_user: CurrentActiveMCPUser):
222217
"""Handle POST messages for a project-specific MCP server with trailing slash."""
223218
# Call the original handler
224219
return await handle_project_messages(project_id, request, current_user)
@@ -228,7 +223,7 @@ async def handle_project_messages_with_slash(
228223
async def update_project_mcp_settings(
229224
project_id: UUID,
230225
settings: list[MCPSettings],
231-
current_user: Annotated[User, Depends(get_current_active_user)],
226+
current_user: CurrentActiveMCPUser,
232227
):
233228
"""Update the MCP settings of all flows in a project."""
234229
try:
@@ -329,7 +324,7 @@ async def install_mcp_config(
329324
project_id: UUID,
330325
body: MCPInstallRequest,
331326
request: Request,
332-
current_user: Annotated[User, Depends(get_current_active_user)],
327+
current_user: CurrentActiveMCPUser,
333328
):
334329
"""Install MCP server configuration for Cursor or Claude."""
335330
# Check if the request is coming from a local IP address
@@ -452,7 +447,7 @@ async def install_mcp_config(
452447
@router.get("/{project_id}/installed")
453448
async def check_installed_mcp_servers(
454449
project_id: UUID,
455-
current_user: Annotated[User, Depends(get_current_active_user)],
450+
current_user: CurrentActiveMCPUser,
456451
):
457452
"""Check if MCP server configuration is installed for this project in Cursor or Claude."""
458453
try:

src/backend/base/langflow/services/auth/utils.py

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
api_key_header = APIKeyHeader(name=API_KEY_NAME, scheme_name="API key header", auto_error=False)
3232

3333
MINIMUM_KEY_LENGTH = 32
34+
AUTO_LOGIN_WARNING = "In v1.6 LANGFLOW_SKIP_AUTH_AUTO_LOGIN will be removed. Please update your authentication method."
35+
AUTO_LOGIN_ERROR = (
36+
"Since v1.5, LANGFLOW_AUTO_LOGIN requires a valid API key. "
37+
"Set LANGFLOW_SKIP_AUTH_AUTO_LOGIN=true to skip this check. "
38+
"Please update your authentication method."
39+
)
3440

3541

3642
# Source: https://github.com/mrtolkien/fastapi_simple_security/blob/master/fastapi_simple_security/security_api_key.py
@@ -49,19 +55,16 @@ async def api_key_security(
4955
status_code=status.HTTP_400_BAD_REQUEST,
5056
detail="Missing first superuser credentials",
5157
)
52-
warnings.warn(
53-
(
54-
"In v1.5, the default behavior of AUTO_LOGIN authentication will change to require a valid API key"
55-
" or JWT. If you integrated with Langflow prior to v1.5, make sure to update your code to pass an "
56-
"API key or JWT when authenticating with protected endpoints."
57-
),
58-
DeprecationWarning,
59-
stacklevel=2,
60-
)
61-
if query_param or header_param:
62-
result = await check_key(db, query_param or header_param)
63-
else:
64-
result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER)
58+
if not query_param and not header_param:
59+
if settings_service.auth_settings.skip_auth_auto_login:
60+
result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER)
61+
logger.warning(AUTO_LOGIN_WARNING)
62+
else:
63+
raise HTTPException(
64+
status_code=status.HTTP_403_FORBIDDEN,
65+
detail=AUTO_LOGIN_ERROR,
66+
)
67+
result = await check_key(db, query_param or header_param)
6568

6669
elif not query_param and not header_param:
6770
raise HTTPException(
@@ -98,15 +101,17 @@ async def ws_api_key_security(
98101
code=status.WS_1011_INTERNAL_ERROR,
99102
reason="Missing first superuser credentials",
100103
)
101-
warnings.warn(
102-
("In v1.5, AUTO_LOGIN will *require* a valid API key or JWT. Please update your clients accordingly."),
103-
DeprecationWarning,
104-
stacklevel=2,
105-
)
106-
if api_key:
107-
result = await check_key(db, api_key)
104+
if not api_key:
105+
if settings.auth_settings.skip_auth_auto_login:
106+
result = await get_user_by_username(db, settings.auth_settings.SUPERUSER)
107+
logger.warning(AUTO_LOGIN_WARNING)
108+
else:
109+
raise WebSocketException(
110+
code=status.WS_1008_POLICY_VIOLATION,
111+
reason=AUTO_LOGIN_ERROR,
112+
)
108113
else:
109-
result = await get_user_by_username(db, settings.auth_settings.SUPERUSER)
114+
result = await check_key(db, api_key)
110115

111116
# normal path: must provide an API key
112117
else:
@@ -473,3 +478,81 @@ def decrypt_api_key(encrypted_api_key: str, settings_service: SettingsService):
473478
)
474479
return fernet.decrypt(encrypted_api_key).decode()
475480
return ""
481+
482+
483+
# MCP-specific authentication functions that always behave as if skip_auth_auto_login is True
484+
async def get_current_user_mcp(
485+
token: Annotated[str, Security(oauth2_login)],
486+
query_param: Annotated[str, Security(api_key_query)],
487+
header_param: Annotated[str, Security(api_key_header)],
488+
db: Annotated[AsyncSession, Depends(get_session)],
489+
) -> User:
490+
"""MCP-specific user authentication that always allows fallback to username lookup.
491+
492+
This function provides authentication for MCP endpoints with special handling:
493+
- If a JWT token is provided, it uses standard JWT authentication
494+
- If no API key is provided and AUTO_LOGIN is enabled, it falls back to
495+
username lookup using the configured superuser credentials
496+
- Otherwise, it validates the provided API key (from query param or header)
497+
"""
498+
if token:
499+
return await get_current_user_by_jwt(token, db)
500+
501+
# MCP-specific authentication logic - always behaves as if skip_auth_auto_login is True
502+
settings_service = get_settings_service()
503+
result: ApiKey | User | None
504+
505+
if settings_service.auth_settings.AUTO_LOGIN:
506+
# Get the first user
507+
if not settings_service.auth_settings.SUPERUSER:
508+
raise HTTPException(
509+
status_code=status.HTTP_400_BAD_REQUEST,
510+
detail="Missing first superuser credentials",
511+
)
512+
if not query_param and not header_param:
513+
# For MCP endpoints, always fall back to username lookup when no API key is provided
514+
result = await get_user_by_username(db, settings_service.auth_settings.SUPERUSER)
515+
if result:
516+
logger.warning(AUTO_LOGIN_WARNING)
517+
return result
518+
else:
519+
result = await check_key(db, query_param or header_param)
520+
521+
elif not query_param and not header_param:
522+
raise HTTPException(
523+
status_code=status.HTTP_403_FORBIDDEN,
524+
detail="An API key must be passed as query or header",
525+
)
526+
527+
elif query_param:
528+
result = await check_key(db, query_param)
529+
530+
else:
531+
result = await check_key(db, header_param)
532+
533+
if not result:
534+
raise HTTPException(
535+
status_code=status.HTTP_403_FORBIDDEN,
536+
detail="Invalid or missing API key",
537+
)
538+
539+
# If result is a User, return it directly
540+
if isinstance(result, User):
541+
return result
542+
543+
# If result is an ApiKey, we need to get the associated user
544+
# This should not happen in normal flow, but adding for completeness
545+
raise HTTPException(
546+
status_code=status.HTTP_403_FORBIDDEN,
547+
detail="Invalid authentication result",
548+
)
549+
550+
551+
async def get_current_active_user_mcp(current_user: Annotated[User, Depends(get_current_user_mcp)]):
552+
"""MCP-specific active user dependency.
553+
554+
This dependency is temporary and will be removed once MCP is fully integrated.
555+
"""
556+
if not current_user.is_active:
557+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user")
558+
return current_user

src/backend/base/langflow/services/settings/auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class AuthSettings(BaseSettings):
5050
COOKIE_DOMAIN: str | None = None
5151
"""The domain attribute of the cookies. If None, the domain is not set."""
5252

53+
skip_auth_auto_login: bool = True
54+
"""If True, the application will skip the authentication auto login, set this to False to revert to pre-v1.5
55+
behavior. This will be removed in v1.6"""
56+
5357
pwd_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
5458

5559
model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_")

src/frontend/tests/core/integrations/starter-projects.spec.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
import { expect, test } from "@playwright/test";
22
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
33

4+
// Helper function to get JWT token for API requests
5+
async function getAuthToken(request: any) {
6+
const formData = new URLSearchParams();
7+
formData.append("username", "langflow");
8+
formData.append("password", "langflow");
9+
10+
const loginResponse = await request.post("/api/v1/login", {
11+
headers: {
12+
"Content-Type": "application/x-www-form-urlencoded",
13+
},
14+
data: formData.toString(),
15+
});
16+
17+
expect(loginResponse.status()).toBe(200);
18+
const tokenData = await loginResponse.json();
19+
return tokenData.access_token;
20+
}
21+
422
test(
523
"vector store from starter projects should have its connections and nodes on the flow",
624
{ tag: ["@release", "@starter-projects"] },
725
async ({ page, request }) => {
8-
const response = await request.get("/api/v1/starter-projects");
26+
// Get authentication token
27+
const authToken = await getAuthToken(request);
28+
29+
const response = await request.get("/api/v1/starter-projects", {
30+
headers: {
31+
Authorization: `Bearer ${authToken}`,
32+
},
33+
});
934
expect(response.status()).toBe(200);
1035
const responseBody = await response.json();
1136

@@ -18,7 +43,13 @@ test(
1843
await page.route("**/api/v1/flows/", async (route) => {
1944
if (route.request().method() === "GET") {
2045
try {
21-
const response = await route.fetch();
46+
// Add authorization header to the request
47+
const headers = route.request().headers();
48+
headers["Authorization"] = `Bearer ${authToken}`;
49+
50+
const response = await route.fetch({
51+
headers: headers,
52+
});
2253
const flowsData = await response.json();
2354

2455
const modifiedFlows = flowsData.map((flow) => {

0 commit comments

Comments
 (0)