Skip to content

feat(auth): external trusted JWT auth + JIT user mapping#13293

Open
erichare wants to merge 1 commit into
release-1.10.0from
feat/oss-external-auth-jit
Open

feat(auth): external trusted JWT auth + JIT user mapping#13293
erichare wants to merge 1 commit into
release-1.10.0from
feat/oss-external-auth-jit

Conversation

@erichare
Copy link
Copy Markdown
Collaborator

@erichare erichare commented May 22, 2026

Summary

Adds OSS support for trusted external identity: accept a JWT (or other credential) issued or validated by an upstream IdP / proxy, and JIT-provision a local Langflow user via the existing SSOUserProfile table. Off by default; opt in with LANGFLOW_EXTERNAL_AUTH_ENABLED=true.

This PR is independent of #13153 / #13290 (RBAC work) — it targets release-1.10.0 directly. Authentication and authorization are separable concerns; RBAC doesn't require this, and this works with or without RBAC. Originally proposed as part of #13280 by @lucaseduoli and @phact.

Credit

The design space — EXTERNAL_AUTH_* settings shape, JWKS / trusted-decode split, JIT mapping via SSOUserProfile, pluggable resolver, and the integration-test patterns — was first proposed in #13280. This PR ports those ideas onto release-1.10.0 directly and routes JIT through the existing BaseAuthService.get_or_create_user_from_claims hook rather than a parallel module. The commit carries Co-Authored-By trailers for both authors.

What's in this PR

  • New EXTERNAL_AUTH_* AuthSettings (off by default) covering provider key, token transport (header / cookie), JWKS or trusted-decode, claim mapping, and a pluggable EXTERNAL_AUTH_IDENTITY_RESOLVER import path.
  • New services/auth/external.py with JWT/JWKS validation, identity resolver protocol, and token-extraction helpers.
  • AuthService.get_or_create_user_from_claims + extract_user_info_from_claims implement the existing BaseAuthService JIT hook through SSOUserProfile — no new tables.
  • _authenticate_with_token falls back to external resolution when the native JWT path fails, so Authorization-header callers transparently upgrade to external auth.
  • Token extractors in services/auth/utils.py consult the configured external header / cookie after the native JWT path on session, WebSocket, SSE, and optional-user dependencies.
  • /api/v1/session catches AuthenticationError so external-credential failures resolve to authenticated=False rather than 500.

Defaults & safety

  • LANGFLOW_EXTERNAL_AUTH_ENABLED=false by default. Zero behavior change unless flipped on.
  • EXTERNAL_AUTH_TRUSTED_JWT_DECODE=false by default. The "decode without signature verification" path exists for deployments where a trusted upstream proxy already validates the JWT — operators must opt in explicitly. JWKS-backed validation via EXTERNAL_AUTH_JWKS_URL is the recommended production path.
  • Native JWT path runs first. External is a fallback after the native Langflow JWT decode fails — no token-confusion risk for existing flows.
  • JIT user creation happens on first request. The local user gets a random-password placeholder (external IdP is the source of truth for credentials) and is recorded in SSOUserProfile(sso_provider, sso_user_id). Subsequent requests reuse the same user.

Why ship separately

Originally this was bundled with the RBAC work in #13290. Splitting it out has three benefits:

  1. Auth and authz are separable concerns. RBAC works fine with native Langflow login / API keys; external auth doesn't require RBAC.
  2. Independent review. External auth touches the security-sensitive request path. Reviewers can focus on the JWT / JWKS / JIT logic without also evaluating Casbin contracts.
  3. Independent rollout. Consumers that need external auth (OpenRAG-style integrations) can adopt this without taking on the RBAC stack, and vice versa.

Configuration

LANGFLOW_EXTERNAL_AUTH_ENABLED=true
LANGFLOW_EXTERNAL_AUTH_PROVIDER=openrag

# Production: verify the JWT signature against the IdP's JWKS.
LANGFLOW_EXTERNAL_AUTH_JWKS_URL=https://idp.example.com/.well-known/jwks.json
LANGFLOW_EXTERNAL_AUTH_ISSUER=https://idp.example.com/
LANGFLOW_EXTERNAL_AUTH_AUDIENCE=langflow
LANGFLOW_EXTERNAL_AUTH_ALGORITHMS=RS256

# Or: trust a sidecar/proxy to validate (off by default — security review required).
# LANGFLOW_EXTERNAL_AUTH_TRUSTED_JWT_DECODE=true

# Claim mapping (defaults shown).
LANGFLOW_EXTERNAL_AUTH_SUBJECT_CLAIM=sub
LANGFLOW_EXTERNAL_AUTH_USERNAME_CLAIM=preferred_username
LANGFLOW_EXTERNAL_AUTH_EMAIL_CLAIM=email
LANGFLOW_EXTERNAL_AUTH_NAME_CLAIM=name

# Optional cookie transport (header is the default).
# LANGFLOW_EXTERNAL_AUTH_TOKEN_COOKIE=langflow-session

For unusual credential formats, point LANGFLOW_EXTERNAL_AUTH_IDENTITY_RESOLVER at a module:attr import path implementing the resolver Protocol (async def resolve(token, auth_settings) -> ExternalIdentity | dict).

Test plan

Green locally:

  • uv run pytest src/backend/tests/unit/services/auth/test_external_auth.py14 passed (JWT decode, claim mapping, custom resolver, header/cookie extraction)
  • uv run pytest src/backend/tests/unit/test_login.py9 passed, includes 3 new integration tests exercising the /api/v1/session JIT path:
    • Header-based JWT → user created + SSOUserProfile row written + second request reuses it.
    • Cookie-based JWT with custom claim mapping → user created with the mapped username.
    • Expired JWT → authenticated=False (no 500).

Pre-merge follow-ups (manual smoke):

  • Boot with LANGFLOW_EXTERNAL_AUTH_ENABLED=true LANGFLOW_EXTERNAL_AUTH_TRUSTED_JWT_DECODE=true LANGFLOW_EXTERNAL_AUTH_PROVIDER=test-provider and curl -H "Authorization: Bearer <jwt>" http://localhost:7860/api/v1/session to confirm the JIT path end-to-end.
  • With LANGFLOW_EXTERNAL_AUTH_JWKS_URL pointed at a real IdP, exercise the full signature-verification path.

What's intentionally not here

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added external trusted-identity authentication with Just-In-Time user provisioning
    • Support for JWT-based authentication from upstream identity providers with configurable claim mapping
    • External credentials now accepted via Authorization headers and cookies
  • Bug Fixes

    • Improved session endpoint error handling for authentication failures

Review Change Stack

Adds the OSS half of trusted external identity support:

- New EXTERNAL_AUTH_* AuthSettings (off by default) covering provider
  key, token transport (header/cookie), JWKS or trusted-decode, claim
  mapping, and a pluggable EXTERNAL_AUTH_IDENTITY_RESOLVER import path.
- New services/auth/external.py with JWT/JWKS validation, identity
  resolver protocol, and token extraction helpers.
- AuthService.get_or_create_user_from_claims +
  extract_user_info_from_claims implement the existing BaseAuthService
  JIT hook through SSOUserProfile - no new tables.
- _authenticate_with_token falls back to external resolution when the
  native JWT path fails, so Authorization-header callers transparently
  upgrade to external auth.
- Token extractors in services/auth/utils.py consult the configured
  external header/cookie after the native JWT path on session,
  WebSocket, SSE, and optional-user dependencies.
- /api/v1/session catches AuthenticationError so external-credential
  failures resolve to authenticated=False rather than 500.

Tests: 14 unit tests for external.py (JWT decode, claim mapping,
custom resolver) plus 3 integration tests in test_login.py exercising
the session endpoint JIT path (header + cookie + expired-token).

Co-Authored-By: phact <estevezsebastian@gmail.com>
Co-Authored-By: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Based-On: #13280
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Walkthrough

This PR introduces external trusted-identity authentication with just-in-time user provisioning. Langflow can now accept external JWTs from upstream identity providers, resolve them to ExternalIdentity objects via configurable resolvers, and automatically create or link local users via SSOUserProfile. The implementation includes JWT validation with optional JWKS-backed verification, configurable claim-to-field mapping, and fallback integration throughout the auth token resolution chain.

Changes

External Trusted-Identity Authentication with JIT Provisioning

Layer / File(s) Summary
Configuration & External Identity Contract
src/lfx/src/lfx/services/settings/auth.py, src/backend/base/langflow/services/auth/external.py
AuthSettings adds fourteen new fields for external auth configuration (enabled toggle, credential sources, resolver path, JWT validation parameters, claim mappings). ExternalIdentity dataclass and ExternalIdentityResolver protocol establish the contract for normalized upstream identity representation.
JWT Validation and Token Extraction
src/backend/base/langflow/services/auth/external.py
Token extraction from headers/cookies with Bearer stripping; JWT validation supporting trusted decode (time-only checks) or JWKS-backed modes with TTL-cached key fetching and kid-based selection; claim normalization to ExternalIdentity via configured field mappings; default resolver implementing JWT-based identity resolution.
Resolver-Based Identity Resolution
src/backend/base/langflow/services/auth/external.py
resolve_external_identity loads pluggable identity resolvers (callable or protocol-based), handles sync/async execution, and converts claim mappings to normalized ExternalIdentity objects for downstream provisioning.
AuthService External Token & JIT Provisioning
src/backend/base/langflow/services/auth/service.py
AuthService falls back to external identity resolution when JWT auth fails. JIT provisioning normalizes claims, retrieves-or-creates SSOUserProfile with email/last-login updates on hits, and generates unique usernames and new inactive accounts on misses with default folder/variable initialization.
Auth Utils Token Fallback Integration
src/backend/base/langflow/services/auth/utils.py
OAuth2PasswordBearerCookie, WebSocket, and SSE current-user helpers consult external tokens as a final fallback after native authorization checks. _get_external_token helper isolates external token extraction logic.
Login Endpoint Session Exception Handling
src/backend/base/langflow/api/v1/login.py
/session endpoint now catches AuthenticationError alongside existing exceptions, returning authenticated=False to bridge external auth errors into the session response contract.
Unit Tests: External Auth Helpers
src/backend/tests/unit/services/auth/test_external_auth.py
Comprehensive unit tests for token extraction, JWT validation (trusted and JWKS modes), claim mapping, and identity resolution with both custom and default resolvers. Includes configurable test fixture and JWT generation helper.
Integration Tests: Login & Session with External Auth
src/backend/tests/unit/test_login.py
Session endpoint integration tests validate JIT user creation from external headers/cookies with claim mapping, profile linking, and user reuse. Tests verify both successful authentication and rejection of expired tokens.

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage ⚠️ Warning Test coverage incomplete: missing unit tests for 6 new AuthService JIT methods, JWKS refresh, WebSocket/SSE flows, bare Bearer edge case, and email preservation on re-login. Add unit tests for AuthService JIT methods, JWKS refresh on key rotation, WebSocket/SSE external auth flows, bare Bearer token handling, and email preservation across re-login attempts.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(auth): external trusted JWT auth + JIT user mapping' clearly and concisely summarizes the main changes: adding external trusted JWT authentication with just-in-time user mapping.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed 14 unit tests in test_external_auth.py and 3 integration tests in test_login.py cover external auth, JIT provisioning, and SSOUserProfile creation with proper naming conventions and assertions.
Test File Naming And Structure ✅ Passed Test files use test_*.py naming, proper pytest structure with fixtures, descriptive names, logical organization with setup/teardown, edge cases, and positive/negative scenarios.
Excessive Mock Usage Warning ✅ Passed Tests show good design: 14 unit tests with real AuthSettings/JWT tokens, only 1 monkeypatch for config loading; 3 integration tests use real client, DB, models. No excessive mocking detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oss-external-auth-jit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the enhancement New feature or request label May 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels May 22, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 62.62295% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.62%. Comparing base (bd2580e) to head (723a636).
⚠️ Report is 1 commits behind head on release-1.10.0.

Files with missing lines Patch % Lines
...rc/backend/base/langflow/services/auth/external.py 68.78% 59 Missing ⚠️
src/backend/base/langflow/services/auth/service.py 39.53% 52 Missing ⚠️
src/backend/base/langflow/services/auth/utils.py 78.57% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.10.0   #13293      +/-   ##
==================================================
+ Coverage           55.57%   55.62%   +0.05%     
==================================================
  Files                2178     2179       +1     
  Lines              205682   205958     +276     
  Branches            29437    31109    +1672     
==================================================
+ Hits               114306   114569     +263     
- Misses              90057    90070      +13     
  Partials             1319     1319              
Flag Coverage Δ
backend 60.34% <60.82%> (+1.40%) ⬆️
frontend 55.47% <ø> (-0.27%) ⬇️
lfx 51.74% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/api/v1/login.py 50.56% <100.00%> (+10.78%) ⬆️
src/lfx/src/lfx/services/settings/auth.py 62.82% <100.00%> (+3.66%) ⬆️
src/backend/base/langflow/services/auth/utils.py 92.30% <78.57%> (+4.96%) ⬆️
src/backend/base/langflow/services/auth/service.py 61.09% <39.53%> (-4.28%) ⬇️
...rc/backend/base/langflow/services/auth/external.py 68.78% <68.78%> (ø)

... and 210 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/base/langflow/services/auth/utils.py (1)

43-57: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Keep the external credential available when a native token is present but invalid.

These helpers reduce auth to a single token string. If a browser sends a stale access_token_lf cookie and a valid external header/cookie, the stale local token wins here, and AuthService's external fallback never sees the real upstream credential. That blocks the advertised “native first, external second” behavior until the old cookie is cleared. Consider threading both candidates through auth, or re-resolving the external credential on native JWT failure.

Also applies to: 212-216, 238-239, 308-309

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/auth/utils.py` around lines 43 - 57, The
current __call__ in the token resolver returns the first present token
(Authorization header or access_token_lf cookie) and never preserves the
external credential returned by _get_external_token, which prevents AuthService
from falling back when the native token is present but later determined invalid;
change the logic so both candidates are made available to AuthService (e.g.,
return a small struct/tuple or attach the external token to the request context)
rather than returning a single string: keep the native token as the primary
candidate (from get_authorization_scheme_param or access_token_lf) but also
capture/return the external value from _get_external_token (or re-resolve it on
native JWT failure) so AuthService can attempt native validation first and then
use the external credential if native validation fails; update all analogous
spots (lines referenced: 212-216, 238-239, 308-309) to follow the same
dual-token pattern and ensure AuthService consumes the combined result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/base/langflow/services/auth/external.py`:
- Around line 134-146: The JWKS cache can serve a stale key after IdP rotation;
modify the logic so that when _select_jwk(...) fails to find a matching kid you
perform a one-time forced refresh of the JWKS and retry before rejecting the
token. Implement this by adding a cache-bypass or force_refresh argument to
_fetch_jwks(jwks_url, force_refresh=False) (or by deleting _jwks_cache[jwks_url]
before calling), then in the code path that selects the JWK (the _select_jwk or
token verification flow) if the kid lookup returns None: clear or bypass the
cached entry, call _fetch_jwks(..., force_refresh=True) to reload, and attempt
the kid lookup once more; only if it still misses then return the original
error.
- Around line 306-309: The current extraction logic returns the whole stripped
header when a bare "Bearer" is provided (scheme == "bearer" and token is empty),
causing the string "Bearer" to be treated as a credential; change the branch so
that when scheme.lower() == "bearer" and token is empty you return None instead
of falling through to return stripped. Locate the code that assigns scheme, _,
token = stripped.partition(" ") (using variables scheme, token, stripped) and
update the conditional to explicitly return None for bare Bearer headers so
downstream logic can try alternate credentials (e.g., cookies).

In `@src/backend/base/langflow/services/auth/service.py`:
- Around line 292-295: The code currently unconditionally assigns profile.email
= identity.email which can erase a previously stored email when
ExternalIdentity.email is omitted; modify the logic in the authentication/update
block that handles ExternalIdentity (the code that sets profile.email,
profile.sso_last_login_at, profile.updated_at) so that you only overwrite
profile.email when identity.email is non-null/non-empty (e.g., check
identity.email is not None/empty before assigning), while still updating
profile.sso_last_login_at and profile.updated_at as before.

---

Outside diff comments:
In `@src/backend/base/langflow/services/auth/utils.py`:
- Around line 43-57: The current __call__ in the token resolver returns the
first present token (Authorization header or access_token_lf cookie) and never
preserves the external credential returned by _get_external_token, which
prevents AuthService from falling back when the native token is present but
later determined invalid; change the logic so both candidates are made available
to AuthService (e.g., return a small struct/tuple or attach the external token
to the request context) rather than returning a single string: keep the native
token as the primary candidate (from get_authorization_scheme_param or
access_token_lf) but also capture/return the external value from
_get_external_token (or re-resolve it on native JWT failure) so AuthService can
attempt native validation first and then use the external credential if native
validation fails; update all analogous spots (lines referenced: 212-216,
238-239, 308-309) to follow the same dual-token pattern and ensure AuthService
consumes the combined result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b1360dd-f11f-4349-b3c8-af9f60eb1000

📥 Commits

Reviewing files that changed from the base of the PR and between 63ee455 and 723a636.

📒 Files selected for processing (8)
  • .secrets.baseline
  • src/backend/base/langflow/api/v1/login.py
  • src/backend/base/langflow/services/auth/external.py
  • src/backend/base/langflow/services/auth/service.py
  • src/backend/base/langflow/services/auth/utils.py
  • src/backend/tests/unit/services/auth/test_external_auth.py
  • src/backend/tests/unit/test_login.py
  • src/lfx/src/lfx/services/settings/auth.py

Comment on lines +134 to +146
async def _fetch_jwks(jwks_url: str) -> dict[str, Any]:
cached = _jwks_cache.get(jwks_url)
now = time.monotonic()
if cached and cached[0] > now:
return cached[1]

async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(jwks_url)
response.raise_for_status()
jwks = response.json()

_jwks_cache[jwks_url] = (now + JWKS_CACHE_TTL_SECONDS, jwks)
return jwks
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh JWKS once on kid misses.

With the current cache behavior, an IdP key rotation can break every new token for up to 300 seconds: _fetch_jwks() returns the stale set, _select_jwk() cannot find the new kid, and we fail without reloading. Please force a one-time JWKS refresh before rejecting the token.

Also applies to: 203-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/auth/external.py` around lines 134 - 146,
The JWKS cache can serve a stale key after IdP rotation; modify the logic so
that when _select_jwk(...) fails to find a matching kid you perform a one-time
forced refresh of the JWKS and retry before rejecting the token. Implement this
by adding a cache-bypass or force_refresh argument to _fetch_jwks(jwks_url,
force_refresh=False) (or by deleting _jwks_cache[jwks_url] before calling), then
in the code path that selects the JWK (the _select_jwk or token verification
flow) if the kid lookup returns None: clear or bypass the cached entry, call
_fetch_jwks(..., force_refresh=True) to reload, and attempt the kid lookup once
more; only if it still misses then return the original error.

Comment on lines +306 to +309
scheme, _, token = stripped.partition(" ")
if scheme.lower() == "bearer" and token:
return token.strip() or None
return stripped
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat bare Bearer headers as missing credentials.

Authorization: Bearer currently falls through and returns "Bearer" as the token. That makes us validate the scheme name itself and, in the header-first path, it also prevents a valid cookie credential from being tried.

Proposed fix
 def extract_bearer_or_raw_token(value: str | None) -> str | None:
     """Strip a 'Bearer ' prefix if present and return the credential."""
     if not value:
         return None
     stripped = value.strip()
     if not stripped:
         return None
     scheme, _, token = stripped.partition(" ")
-    if scheme.lower() == "bearer" and token:
-        return token.strip() or None
+    if scheme.lower() == "bearer":
+        return token.strip() or None
     return stripped
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scheme, _, token = stripped.partition(" ")
if scheme.lower() == "bearer" and token:
return token.strip() or None
return stripped
scheme, _, token = stripped.partition(" ")
if scheme.lower() == "bearer":
return token.strip() or None
return stripped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/auth/external.py` around lines 306 - 309,
The current extraction logic returns the whole stripped header when a bare
"Bearer" is provided (scheme == "bearer" and token is empty), causing the string
"Bearer" to be treated as a credential; change the branch so that when
scheme.lower() == "bearer" and token is empty you return None instead of falling
through to return stripped. Locate the code that assigns scheme, _, token =
stripped.partition(" ") (using variables scheme, token, stripped) and update the
conditional to explicitly return None for bare Bearer headers so downstream
logic can try alternate credentials (e.g., cookies).

Comment on lines +292 to +295
now = datetime.now(timezone.utc)
profile.email = identity.email
profile.sso_last_login_at = now
profile.updated_at = now
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't clear the stored email when the upstream token omits it.

ExternalIdentity.email is optional, so this assignment erases a previously known address on any later login where the resolver does not emit email. Preserve the existing value unless a non-null email is supplied.

Proposed fix
             now = datetime.now(timezone.utc)
-            profile.email = identity.email
+            if identity.email is not None:
+                profile.email = identity.email
             profile.sso_last_login_at = now
             profile.updated_at = now
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/auth/service.py` around lines 292 - 295,
The code currently unconditionally assigns profile.email = identity.email which
can erase a previously stored email when ExternalIdentity.email is omitted;
modify the logic in the authentication/update block that handles
ExternalIdentity (the code that sets profile.email, profile.sso_last_login_at,
profile.updated_at) so that you only overwrite profile.email when identity.email
is non-null/non-empty (e.g., check identity.email is not None/empty before
assigning), while still updating profile.sso_last_login_at and
profile.updated_at as before.

@github-actions
Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 39%
39.72% (50428/126937) 68.38% (6866/10040) 39.1% (1139/2913)

Unit Test Results

Tests Skipped Failures Errors Time
4404 0 💤 0 ❌ 0 🔥 9m 45s ⏱️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant