Skip to content

Commit 1c198c2

Browse files
Bharani0012Saravana Kumar RajendranKabilan-16
authored
Clerk only multi org implementation (#67)
* revert env example to exclude Clerk settings * chore: store Clerk token in cookie (#14) * Wrap logout mutation with Clerk signout (#15) * refactor: move clerk mock mutation to util (#16) * restore env example and clean doc (#17) * docs: move clerk env vars to integration guide (#18) * fix: replace logout import path (#19) * Clerk auth frontend (#22) * refactor: update auth and login pages to signup users * updated imports in clerk_utils.py * modified clerk_auth and login-page.tsx * updated context-wrapper * updated context-wrapper * modified use-post-refresh-access.ts * add new file for clerk constants * modified autologin for clerk auth * add token refresh effect in auth.tsx * Ruff check resolved * Ruff check resolve changes * imported the enum and remove the logs * removed constant file * update imports in login page.tsx * updated imports in index.tsx file * update imports in use get auto login.tsx file * updated imports in use post refersh access.tsx file * Clerk token verify (#28) * added protected paths in login api * added bearer token in header * ruff check fix * update log level for auth, login-pages, use-get-autologin * signup comment modified * Add Clerk auth settings (#30) * chore: remove organisation service tests * Refine Clerk token handling * refactor: simplify org db service retrieval * refactor: allow disabling org db service * feat: provide organisation-agnostic db session * feat(auth): add multi-tenant setup for Langflow with Clerk integration - Introduced multi-organization flow in frontend (auth guards, routes, org page) - Updated backend services (auth, organisation, deps) for Clerk multi-tenant support - Synced organization creation with frontend API hooks - Improved initial app setup for multi-org handling * modified auth.tsx * modified OrganizationPage.tsx * modified token sync in auth.tsx * modified auth.tsx * modified clerk_utils, auth.tsx, organizationPage.tsx --------- Co-authored-by: Saravana Kumar Rajendran <srajendran@microsoft.com> Co-authored-by: unknown <kabik5095@gmail.com> Co-authored-by: Kabilan A <147593493+Kabilan-16@users.noreply.github.com>
1 parent b71780f commit 1c198c2

26 files changed

Lines changed: 819 additions & 335 deletions

File tree

src/backend/base/langflow/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ def superuser(
472472
) -> None:
473473
"""Create a superuser."""
474474
configure(log_level=log_level)
475-
db_service = get_db_service()
475+
db_service = get_db_service(use_organisation=False)
476476

477477
async def _create_superuser():
478478
await initialize_services()
@@ -540,7 +540,7 @@ def copy_db() -> None:
540540

541541
async def _migration(*, test: bool, fix: bool) -> None:
542542
await initialize_services(fix_migration=fix)
543-
db_service = get_db_service()
543+
db_service = get_db_service(use_organisation=False)
544544
if not test:
545545
await db_service.run_migrations()
546546
results = await db_service.run_migrations_test()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from pydantic import BaseModel
66
from sqlmodel import select
77

8-
from langflow.api.utils import DbSession
98
from langflow.services.database.models.flow import Flow
109
from langflow.services.deps import get_chat_service
10+
from langflow.services.deps_no_org import DbNoOrgSession
1111

1212
health_check_router = APIRouter(tags=["Health Check"])
1313

@@ -38,7 +38,7 @@ async def health():
3838
# It's a reliable health check for a langflow instance
3939
@health_check_router.get("/health_check")
4040
async def health_check(
41-
session: DbSession,
41+
session: DbNoOrgSession,
4242
) -> HealthResponse:
4343
response = HealthResponse()
4444
# use a fixed valid UUId that UUID collision is very unlikely

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
mcp_projects_router,
1313
mcp_router,
1414
monitor_router,
15+
organisation_router,
1516
projects_router,
1617
starter_projects_router,
1718
store_router,
@@ -47,6 +48,7 @@
4748
router_v1.include_router(monitor_router)
4849
router_v1.include_router(folders_router)
4950
router_v1.include_router(projects_router)
51+
router_v1.include_router(organisation_router)
5052
router_v1.include_router(starter_projects_router)
5153
router_v1.include_router(voice_mode_router)
5254
router_v1.include_router(mcp_router)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from langflow.api.v1.mcp import router as mcp_router
99
from langflow.api.v1.mcp_projects import router as mcp_projects_router
1010
from langflow.api.v1.monitor import router as monitor_router
11+
from langflow.api.v1.organisation_router import router as organisation_router
1112
from langflow.api.v1.projects import router as projects_router
1213
from langflow.api.v1.starter_projects import router as starter_projects_router
1314
from langflow.api.v1.store import router as store_router
@@ -27,6 +28,7 @@
2728
"mcp_projects_router",
2829
"mcp_router",
2930
"monitor_router",
31+
"organisation_router",
3032
"projects_router",
3133
"starter_projects_router",
3234
"store_router",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async def list_project_tools(
6767
"""List all tools in a project that are enabled for MCP."""
6868
tools: list[MCPSettings] = []
6969
try:
70-
async with session_scope() as session:
70+
async with session_scope(use_organisation=False) as session:
7171
# Fetch the project first to verify it exists and belongs to the current user
7272
project = (
7373
await session.exec(
@@ -138,7 +138,7 @@ async def handle_project_sse(
138138
):
139139
"""Handle SSE connections for a specific project."""
140140
# Verify project exists and user has access
141-
async with session_scope() as session:
141+
async with session_scope(use_organisation=False) as session:
142142
project = (
143143
await session.exec(select(Folder).where(Folder.id == project_id, Folder.user_id == current_user.id))
144144
).first()
@@ -746,7 +746,7 @@ def get_project_mcp_server(project_id: UUID) -> ProjectMCPServer:
746746
async def init_mcp_servers():
747747
"""Initialize MCP servers for all projects."""
748748
try:
749-
async with session_scope() as session:
749+
async with session_scope(use_organisation=False) as session:
750750
projects = (await session.exec(select(Folder))).all()
751751

752752
for project in projects:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import APIRouter, HTTPException
2+
3+
from langflow.services.database.organisation import OrganizationService
4+
from langflow.services.deps import get_settings_service
5+
6+
router = APIRouter(tags=["Organisation"])
7+
8+
9+
@router.post("/create_organisation")
10+
async def create_organisation():
11+
"""Create a new organisation database."""
12+
settings_service = get_settings_service()
13+
if not settings_service.auth_settings.CLERK_AUTH_ENABLED:
14+
raise HTTPException(status_code=404, detail="Not found")
15+
16+
service = OrganizationService()
17+
try:
18+
await service.create_database_and_tables_other_initializations_with_org()
19+
except Exception as exc:
20+
raise HTTPException(status_code=500, detail=str(exc)) from exc
21+
return {"detail": "Organisation database created"}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def add_user(
3131
"""Add a new user to the database."""
3232
new_user = User.model_validate(user, from_attributes=True)
3333
try:
34-
await process_new_user_with_clerk(user, new_user)
34+
await process_new_user_with_clerk(new_user)
3535
new_user.password = get_password_hash(user.password)
3636
new_user.is_active = get_settings_service().auth_settings.NEW_USER_IS_ACTIVE
3737
session.add(new_user)

src/backend/base/langflow/initial_setup/setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ async def load_flows_from_directory() -> None:
708708
logger.warning("AUTO_LOGIN is disabled, not loading flows from directory")
709709
return
710710

711-
async with session_scope() as session:
711+
async with session_scope(use_organisation=False) as session:
712712
user = await get_user_by_username(session, settings_service.auth_settings.SUPERUSER)
713713
if user is None:
714714
msg = "Superuser not found in the database"
@@ -767,7 +767,7 @@ async def load_bundles_from_urls() -> tuple[list[TemporaryDirectory], list[str]]
767767
if not settings_service.auth_settings.AUTO_LOGIN:
768768
logger.warning("AUTO_LOGIN is disabled, not loading flows from URLs")
769769

770-
async with session_scope() as session:
770+
async with session_scope(use_organisation=False) as session:
771771
user = await get_user_by_username(session, settings_service.auth_settings.SUPERUSER)
772772
if user is None:
773773
msg = "Superuser not found in the database"
@@ -877,7 +877,7 @@ async def create_or_update_starter_projects(all_types_dict: dict, *, do_create:
877877
all_types_dict (dict): Dictionary containing all component types and their templates
878878
do_create (bool, optional): Whether to create new projects. Defaults to True.
879879
"""
880-
async with session_scope() as session:
880+
async with session_scope(use_organisation=False) as session:
881881
new_folder = await create_starter_folder(session)
882882
starter_projects = await load_starter_projects()
883883
await delete_start_projects(session, new_folder.id)
@@ -934,7 +934,7 @@ async def initialize_super_user_if_needed() -> None:
934934
msg = "SUPERUSER and SUPERUSER_PASSWORD must be set in the settings if AUTO_LOGIN is true."
935935
raise ValueError(msg)
936936

937-
async with session_scope() as async_session:
937+
async with session_scope(use_organisation=False) as async_session:
938938
super_user = await create_super_user(db=async_session, username=username, password=password)
939939
await get_variable_service().initialize_user_variables(super_user.id, async_session)
940940
_ = await get_or_create_default_folder(async_session, super_user.id)
@@ -983,7 +983,7 @@ async def sync_flows_from_fs():
983983
fs_flows_polling_interval = get_settings_service().settings.fs_flows_polling_interval / 1000
984984
while True:
985985
try:
986-
async with session_scope() as session:
986+
async with session_scope(use_organisation=False) as session:
987987
stmt = select(Flow).where(col(Flow.fs_path).is_not(None))
988988
flows = (await session.exec(stmt)).all()
989989
for flow in flows:

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

Lines changed: 56 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from starlette.status import HTTP_401_UNAUTHORIZED
1212

1313
from langflow.logging.logger import logger
14-
from langflow.services.database.models.user import User, UserCreate
14+
from langflow.services.database.models.user import User
1515
from langflow.services.database.models.user.crud import get_user_by_id
1616
from langflow.services.deps import get_settings_service
1717

@@ -20,9 +20,6 @@
2020

2121
_jwks_cache: dict[str, dict[str, Any]] = {}
2222

23-
# APIs that require Clerk token decoding in middleware
24-
PROTECTED_PATHS = ["/api/v1/users/","/api/v1/login"]
25-
2623

2724
async def _get_jwks(issuer: str) -> dict[str, Any]:
2825
"""Retrieve and cache JWKS for a Clerk issuer."""
@@ -64,6 +61,7 @@ async def verify_clerk_token(token: str) -> dict[str, Any]:
6461
algorithms=[unverified_header.get("alg", "RS256")],
6562
audience=unverified_claims.get("aud"),
6663
issuer=issuer,
64+
# options={"verify_signature": False, "verify_aud": False, "verify_exp": False},
6765
)
6866
# ✅ Add deterministic UUID to the payload
6967
clerk_id = payload.get("sub")
@@ -72,54 +70,54 @@ async def verify_clerk_token(token: str) -> dict[str, Any]:
7270
raise JWTError(msg)
7371
payload["uuid"] = str(uuid.uuid5(uuid.NAMESPACE_DNS, str(clerk_id)))
7472

73+
org = payload.get("o")
74+
if isinstance(org, dict) and "id" in org:
75+
payload["org_id"] = org["id"]
76+
elif "org_id" in payload:
77+
# Some Clerk tokens expose the organisation id directly
78+
payload["org_id"] = payload["org_id"]
79+
else:
80+
msg = "Missing organization info in Clerk token payload"
81+
raise JWTError(msg)
82+
logger.info(f"[ClerkAuthAdapter] Verified Clerk token for org_id: {payload}")
7583
except JWTError as exc:
7684
msg = "Invalid token"
7785
raise ValueError(msg) from exc
7886
return payload
7987

8088

81-
async def process_new_user_with_clerk(_user: UserCreate, new_user: User):
82-
settings = get_settings_service().auth_settings
83-
# ✅ If Clerk is enabled, pull UUID from enriched auth_header_ctx payload
84-
if settings.CLERK_AUTH_ENABLED:
85-
payload = auth_header_ctx.get()
86-
if not payload:
87-
raise HTTPException(status_code=401, detail="Missing Clerk payload")
88-
clerk_uuid = payload.get("uuid")
89-
if not clerk_uuid:
90-
raise HTTPException(status_code=401, detail="Missing Clerk UUID")
91-
new_user.id = UUID(clerk_uuid)
92-
logger.info(f"[process_new_user_with_clerk] Assigned Clerk UUID {new_user.id} to new user object")
93-
94-
async def get_user_from_clerk_payload(token: str, db: AsyncSession) -> User:
95-
"""Retrieve the current user using the payload from ``verify_clerk_token``."""
89+
def get_user_id_from_clerk_payload() -> UUID:
90+
"""Extract the Clerk user UUID from the request context."""
91+
payload = auth_header_ctx.get()
92+
if not payload:
93+
raise HTTPException(status_code=401, detail="Missing Clerk payload")
94+
clerk_uuid = payload.get("uuid")
95+
if not clerk_uuid:
96+
raise HTTPException(status_code=401, detail="Missing Clerk UUID")
9697
try:
97-
payload = await verify_clerk_token(token)
98-
except Exception as exc:
99-
raise HTTPException(
100-
status_code=status.HTTP_401_UNAUTHORIZED,
101-
detail="Authentication failed",
102-
headers={"WWW-Authenticate": "Bearer"},
103-
) from exc
104-
105-
uuid_str = payload.get("uuid")
106-
logger.info(f"uuid_str: {uuid_str}")
107-
if not uuid_str:
108-
raise HTTPException(
109-
status_code=status.HTTP_401_UNAUTHORIZED,
110-
detail="Missing Clerk UUID",
111-
headers={"WWW-Authenticate": "Bearer"},
112-
)
113-
114-
try:
115-
user_id = UUID(uuid_str)
98+
return UUID(clerk_uuid)
11699
except ValueError as err:
117100
raise HTTPException(
118-
status_code=status.HTTP_401_UNAUTHORIZED,
101+
status_code=401,
119102
detail="Invalid Clerk UUID format",
120103
headers={"WWW-Authenticate": "Bearer"},
121104
) from err
122105

106+
107+
async def process_new_user_with_clerk(new_user: User):
108+
settings = get_settings_service().auth_settings
109+
# ✅ If Clerk is enabled, pull UUID from enriched auth_header_ctx payload
110+
if settings.CLERK_AUTH_ENABLED:
111+
user_id = get_user_id_from_clerk_payload()
112+
new_user.id = user_id
113+
logger.info(f"[process_new_user_with_clerk] Assigned Clerk UUID {new_user.id} to new user object")
114+
115+
116+
async def get_user_from_clerk_payload(db: AsyncSession) -> User:
117+
"""Retrieve the current user using the payload stored in the request context."""
118+
user_id = get_user_id_from_clerk_payload()
119+
logger.debug(f"uuid_str: {user_id}")
120+
123121
user = await get_user_by_id(db, user_id)
124122
logger.info(f"Retrieved user: {user}")
125123
if user is None:
@@ -142,36 +140,31 @@ async def get_user_from_clerk_payload(token: str, db: AsyncSession) -> User:
142140

143141

144142
async def clerk_token_middleware(request: Request, call_next):
145-
"""Middleware to decode Clerk token for specific paths."""
143+
"""Middleware to decode Clerk token when present."""
146144
settings = get_settings_service()
145+
if not settings.auth_settings.CLERK_AUTH_ENABLED:
146+
return await call_next(request)
147147

148-
ctx_token: Token | None = None
149-
if settings.auth_settings.CLERK_AUTH_ENABLED and request.url.path in PROTECTED_PATHS:
150-
auth_header = request.headers.get("Authorization")
151-
152-
if not auth_header or not auth_header.startswith("Bearer "):
153-
logger.warning("Missing or malformed Authorization header for Clerk protected route.")
154-
return JSONResponse(
155-
status_code=HTTP_401_UNAUTHORIZED,
156-
content={"detail": "Authorization header with valid Bearer token required"},
157-
)
158-
159-
if auth_header and auth_header.startswith("Bearer "):
160-
token = auth_header[len("Bearer ") :]
161-
try:
162-
payload = await verify_clerk_token(token)
163-
ctx_token = auth_header_ctx.set(payload)
164-
except Exception as exc: # noqa: BLE001
165-
logger.warning(f"Failed to verify Clerk token: {exc}")
166-
return JSONResponse(
167-
status_code=HTTP_401_UNAUTHORIZED,
168-
content={"detail": "Invalid Clerk token"}
169-
)
148+
auth_header = request.headers.get("Authorization")
149+
logger.info(f"Authorization header present: {auth_header}")
150+
if not auth_header or not auth_header.startswith("Bearer "):
151+
return await call_next(request)
170152

153+
ctx_token: Token | None = None
154+
token = auth_header[len("Bearer ") :]
171155
try:
172-
return await call_next(request)
156+
payload = await verify_clerk_token(token)
157+
ctx_token = auth_header_ctx.set(payload)
158+
response = await call_next(request)
159+
except Exception as exc: # noqa: BLE001
160+
logger.warning(f"Failed to verify Clerk token: {exc}")
161+
return JSONResponse(
162+
status_code=HTTP_401_UNAUTHORIZED,
163+
content={"detail": "Invalid Clerk token"},
164+
)
173165
finally:
174166
if ctx_token is not None:
175167
auth_header_ctx.reset(ctx_token)
176168
else:
177169
auth_header_ctx.set(None)
170+
return response

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def get_current_user_by_jwt(
164164
token = await token
165165

166166
if settings_service.auth_settings.CLERK_AUTH_ENABLED:
167-
return await get_user_from_clerk_payload(token, db)
167+
return await get_user_from_clerk_payload(db)
168168

169169
secret_key = settings_service.auth_settings.SECRET_KEY.get_secret_value()
170170
if secret_key is None:

0 commit comments

Comments
 (0)