Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.clerk.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Clerk authentication
# When true, the frontend uses Clerk for login and sign up
CLERK_AUTH_ENABLED=

# Publishable key for Clerk when authentication is enabled
CLERK_PUBLISHABLE_KEY=
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ DOCKERFILE=docker/build_and_push.Dockerfile
DOCKERFILE_BACKEND=docker/build_and_push_backend.Dockerfile
DOCKERFILE_FRONTEND=docker/frontend/build_and_push_frontend.Dockerfile
DOCKER_COMPOSE=docker_example/docker-compose.yml

VITE_CLERK_AUTH_ENABLED ?= false
VITE_CLERK_PUBLISHABLE_KEY ?=
PYTHON_REQUIRED=$(shell grep '^requires-python[[:space:]]*=' pyproject.toml | sed -n 's/.*"\([^"]*\)".*/\1/p')
RED=\033[0;31m
NC=\033[0m # No Color
Expand Down Expand Up @@ -333,6 +336,9 @@ dockerfile_build:
@echo 'BUILDING DOCKER IMAGE: ${DOCKERFILE}'
@docker build --rm \
-f ${DOCKERFILE} \
--build-arg VITE_CLERK_AUTH_ENABLED=${VITE_CLERK_AUTH_ENABLED} \
--build-arg VITE_CLERK_PUBLISHABLE_KEY=${VITE_CLERK_PUBLISHABLE_KEY} \
--build-arg VITE_AUTO_LOGIN=$(VITE_AUTO_LOGIN) \
-t langflow:${VERSION} .

dockerfile_build_be: dockerfile_build
Expand Down
9 changes: 9 additions & 0 deletions docker/build_and_push.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \

COPY ./src /app/src

ARG VITE_AUTO_LOGIN=true
ENV VITE_AUTO_LOGIN=$VITE_AUTO_LOGIN

COPY src/frontend /tmp/src/frontend
WORKDIR /tmp/src/frontend

ARG VITE_CLERK_AUTH_ENABLED=false
ARG VITE_CLERK_PUBLISHABLE_KEY=""
ENV VITE_CLERK_AUTH_ENABLED=$VITE_CLERK_AUTH_ENABLED
ENV VITE_CLERK_PUBLISHABLE_KEY=$VITE_CLERK_PUBLISHABLE_KEY

RUN --mount=type=cache,target=/root/.npm \
npm ci \
&& npm run build \
Expand Down
40 changes: 40 additions & 0 deletions docs/docs/Integrations/clerk-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Clerk Authentication
slug: /clerk-auth
---


This page summarizes how Langflow handles access token refreshes in different authentication modes and what state changes occur when using Clerk.

## Refresh scenarios when Clerk authentication is disabled

When `CLERK_AUTH_ENABLED` is `false`, Langflow relies on its internal authentication. Tokens are refreshed in these situations:

1. **Protected routes** – the `ProtectedRoute` component periodically calls the `/refresh` endpoint using the `useRefreshAccessToken` hook.
2. **API requests** – the API interceptor automatically triggers the same hook when a request fails with a `401` or `403` error.

Both cases invoke the `useRefreshAccessToken` hook which posts to `/refresh` and updates the refresh token cookie.
The default interval used by `ProtectedRoute` is defined by `LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS` (54 minutes by default) or the value of the `ACCESS_TOKEN_EXPIRE_SECONDS` environment variable.

## State updates when using Clerk

When `CLERK_AUTH_ENABLED` is `true`, token refresh from the frontend is skipped. After signing in with Clerk, `ClerkAuthAdapter` logs the user in to the backend and calls `login()` from the authentication context, which updates several states:

| State location | Value after Clerk token update |
|----------------|--------------------------------|
| `access_token_lf` cookie | Clerk session token |
| `refresh_token_lf` cookie | Backend refresh token |
| `auto_login_lf` cookie | `"login"` |
| Local storage `access_token_lf` | Clerk session token |
| `authStore.accessToken` | Clerk session token |
| `authStore.isAuthenticated` | `true` |
| `authContext.userData` | populated from `/users/whoami` |

With Clerk enabled the periodic refresh is disabled and all further state changes rely on Clerk sessions.

## Environment variables

| Variable | Format | Default | Description |
|----------|--------|---------|-------------|
| <Link id="CLERK_AUTH_ENABLED"/>CLERK_AUTH_ENABLED | Boolean | `false` | When enabled, Langflow uses Clerk for authentication. After signing in or signing up with Clerk, the frontend registers the user with the backend and then logs in via `/api/v1/login` to sync cookies. The `access_token_lf` cookie stores the Clerk session token. |
| <Link id="CLERK_PUBLISHABLE_KEY"/>CLERK_PUBLISHABLE_KEY | String | Not set | Publishable key required when `CLERK_AUTH_ENABLED` is `true`. |
18 changes: 16 additions & 2 deletions src/backend/base/langflow/services/auth/clerk_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

import httpx
from fastapi import HTTPException, Request, status
from fastapi.responses import JSONResponse
from jose import JWTError, jwk, jwt
from sqlmodel.ext.asyncio.session import AsyncSession
from src.backend.base.langflow.logging.logger import logger
from starlette.status import HTTP_401_UNAUTHORIZED

from langflow.logging.logger import logger
from langflow.services.database.models.user import User, UserCreate
from langflow.services.database.models.user.crud import get_user_by_id
from langflow.services.deps import get_settings_service
Expand All @@ -19,7 +21,7 @@
_jwks_cache: dict[str, dict[str, Any]] = {}

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


async def _get_jwks(issuer: str) -> dict[str, Any]:
Expand Down Expand Up @@ -146,13 +148,25 @@ async def clerk_token_middleware(request: Request, call_next):
ctx_token: Token | None = None
if settings.auth_settings.CLERK_AUTH_ENABLED and request.url.path in PROTECTED_PATHS:
auth_header = request.headers.get("Authorization")

if not auth_header or not auth_header.startswith("Bearer "):
logger.warning("Missing or malformed Authorization header for Clerk protected route.")
return JSONResponse(
status_code=HTTP_401_UNAUTHORIZED,
content={"detail": "Authorization header with valid Bearer token required"},
)

if auth_header and auth_header.startswith("Bearer "):
token = auth_header[len("Bearer ") :]
try:
payload = await verify_clerk_token(token)
ctx_token = auth_header_ctx.set(payload)
except Exception as exc: # noqa: BLE001
logger.warning(f"Failed to verify Clerk token: {exc}")
return JSONResponse(
status_code=HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid Clerk token"}
)

try:
return await call_next(request)
Expand Down
3 changes: 3 additions & 0 deletions src/backend/base/langflow/services/settings/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class AuthSettings(BaseSettings):
COOKIE_DOMAIN: str | None = None
"""The domain attribute of the cookies. If None, the domain is not set."""

CLERK_AUTH_ENABLED: bool = False
CLERK_PUBLISHABLE_KEY: str | None = None

pwd_context: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")

model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_")
Expand Down
95 changes: 95 additions & 0 deletions src/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@chakra-ui/number-input": "^2.1.2",
"@clerk/clerk-react": "^5.32.4",
"@headlessui/react": "^2.0.4",
"@hookform/resolvers": "^3.6.0",
"@million/lint": "^1.0.0-rc.26",
Expand Down
Loading
Loading