Skip to content

feat(device-flow): redirect SSO-complete failures to a dedicated device error view#38185

Draft
GareArc wants to merge 3 commits into
mainfrom
fix/device-flow-sso-error-ux
Draft

feat(device-flow): redirect SSO-complete failures to a dedicated device error view#38185
GareArc wants to merge 3 commits into
mainfrom
fix/device-flow-sso-error-ux

Conversation

@GareArc

@GareArc GareArc commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Problem

When an external SSO login fails during the OAuth device flow, sso-complete raised BadRequest/Conflict → a raw werkzeug error page, and the /device page only showed an inline banner on the code-entry screen (never scrubbed from the URL). Most failures showed a raw page; the CLI received no signal and polled until expiry.

What this does (Python + web half)

  • B1 (api)sso-complete now returns a 302 redirect on every failure to a relative /device?sso_error=...&user_code=.... Adds _device_error_redirect (clamps the code to {sso_failed, email_belongs_to_dify_account}, relative location, urlencoded) and a catch-all try/except that logs the full exception server-side and redirects generic — so no unforeseen raise can leak a raw page. user_code is extracted from claims before the nonce is consumed so a replay redirect still carries it. Email special case keeps its audit emit; success path unchanged.
  • C1 (web) — dedicated full-screen error_sso view (replaces the inline banner), scrubs sso_error+user_code from the URL on mount, and a single "← Back to login options" button that re-checks the code (valid → chooser, dead → expired, none → code entry). Raw error code is never rendered — copy comes from the existing ssoErrorCopy table.

Cross-repo seam

Go enterprise relays a failure to <idp_callback_url>?sso_error=sso_failed&user_code=<code>; Python owns the single relative /device mapping. Companion PR: langgenius/dify-enterprise fix/device-flow-sso-error-ux.

Security / invariants

  • No raw backend string crosses to the browser — Python clamps the code, web copy-table falls back to generic for anything unknown.
  • /device redirects stay relative (same-origin proxy convention).
  • A failed SSO attempt burns only the per-attempt nonce; the device user_code stays PENDING (error paths are read-only on device state).

Tests

pytest tests/unit_tests/controllers/openapi/test_device_sso.py 15/15; web vitest app/device 12/12. Two pre-existing claim tests updated 400→302 (now also assert the redirect target — stricter).

Follow-up (not blocking)

  • PENDING-after-error integration test (needs a Redis-backed harness).

GareArc added 2 commits June 29, 2026 19:18
All failure paths in sso_complete now return 302 redirects to
/device?sso_error=... instead of raw werkzeug error pages. Adds
_device_error_redirect helper that clamps codes to the two allowed
values, an outer catch-all try/except that logs + redirects on
unhandled exceptions, and relays any inbound sso_error from Go
enterprise straight through. user_code is now extracted before
the nonce is consumed so replay redirects can still carry it.
@github-actions github-actions Bot added the web This relates to changes on the web. label Jun 30, 2026
params: dict[str, str] = {"sso_error": safe_code}
if user_code:
params["user_code"] = user_code
return redirect(f"/device?{urlencode(params)}", code=302)
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 51.49% 51.48% -0.01%
Strict coverage 51.00% 50.99% -0.01%
Typed symbols 30,849 30,849 0
Untyped symbols 29,343 29,354 +11
Modules 2932 2932 0

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-06-30 03:16:18.460885769 +0000
+++ /tmp/pyrefly_pr.txt	2026-06-30 03:16:03.777897455 +0000
@@ -2297,9 +2297,9 @@
 ERROR Object of class `NoneType` has no attribute `total_tokens` [missing-attribute]
   --> tests/unit_tests/controllers/openapi/test_models.py:13:12
 ERROR Argument `test_verify_approval_grant_raises_on_missing_field._FakeKeyset` is not assignable to parameter `keyset` with type `KeySet` in function `libs.jws.sign` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/test_oauth_sso_claims.py:67:9
+  --> tests/unit_tests/controllers/openapi/test_oauth_sso_claims.py:69:9
 ERROR Argument `test_verify_approval_grant_raises_on_missing_field._FakeKeyset` is not assignable to parameter `keyset` with type `KeySet` in function `libs.device_flow_security.verify_approval_grant` [bad-argument-type]
-  --> tests/unit_tests/controllers/openapi/test_oauth_sso_claims.py:73:52
+  --> tests/unit_tests/controllers/openapi/test_oauth_sso_claims.py:75:52
 ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:38:12
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.08696% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.33%. Comparing base (cb35c6f) to head (f903b98).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
api/controllers/openapi/oauth_device_sso.py 66.66% 9 Missing ⚠️
web/app/device/page.tsx 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #38185      +/-   ##
==========================================
- Coverage   85.34%   85.33%   -0.02%     
==========================================
  Files        4967     4967              
  Lines      258448   258548     +100     
  Branches    49042    49064      +22     
==========================================
+ Hits       220582   220626      +44     
- Misses      33573    33630      +57     
+ Partials     4293     4292       -1     
Flag Coverage Δ
api 85.46% <66.66%> (-0.04%) ⬇️
dify-ui 94.93% <ø> (ø)
web 85.05% <89.47%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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.

…assert

Validate user_code against a [A-Z0-9-] charset before placing it in the
/device redirect, closing the CodeQL url-redirection finding (the value
can no longer carry path/scheme separators). Split the compound assert
flagged by ruff PT018.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants