Skip to content

fix(auth): redirect OAuth login failures to login page instead of raw JSON (#1273) - #2955

Open
dokterbob wants to merge 7 commits into
mainfrom
fix/oauth-error-ux-1273
Open

fix(auth): redirect OAuth login failures to login page instead of raw JSON (#1273)#2955
dokterbob wants to merge 7 commits into
mainfrom
fix/oauth-error-ux-1273

Conversation

@dokterbob

@dokterbob dokterbob commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • All user-facing OAuth callback failures (oauth_callback and oauth_azure_hf_callback) now redirect to /login?error=oauthSignin instead of returning raw JSON or a bare 500 — fixes the screenshot in UX bug: raw JSON on OAuth errors / Exception handling in auth #1273
  • Missing code/state and invalid state cookie were HTTPException (400/401), now redirect; developer-config errors (missing oauth_callback, unknown provider) remain HTTPException
  • Frontend i18n lookup tries the raw error key before lowercasing, so camelCase keys like oauthSignin now resolve to their specific message ("Try signing in with a different account") instead of always falling back to the generic default

Test plan

  • 8 new backend unit tests (backend/tests/test_server.py) — all failure paths for both callback handlers
  • New Cypress E2E spec (cypress/e2e/oauth_auth/) — provider error redirect, invalid-state redirect, friendly message render at /login?error=oauthSignin
  • All existing tests green (uv run pytest tests/test_server.py — 61 passed)
  • Lint, format, type-check clean (pre-commit hooks passed on commit)

Closes #1273

🤖 Generated with Claude Code


Summary by cubic

Redirects all OAuth login failures to the login page with a localized oauthSignin message and correct redirect codes (302 for GET, 303 for POST). Fixes #1273.

  • Bug Fixes
    • All user-facing failures in oauth_callback (GET) and oauth_azure_hf_callback (POST) now redirect to /login?error=oauthSignin; dev-config errors remain HTTPException. Maps provider errors (incl. access_denied) to the friendly key, reads Azure error from the form body (with query fallback), re-raises asyncio.CancelledError, and logs invalid state as a warning with exc_info=True.
    • Frontend: LoginForm tries the raw error key before lowercasing so oauthSignin resolves correctly; Alert uses role="alert" for errors and role="status" otherwise. Updated oauthSignin copy across all locales to: "Sign in failed. Please try again, or use a different sign-in method."
    • Tests: Added backend unit tests and Cypress E2E to cover redirects, assert the error=oauthSignin key, verify exact status codes (302 for GET, 303 for POST), and check Azure form-post error logging.

Written for commit 16f4802. Summary will update on new commits.

Review in cubic

Copilot AI left a comment

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.

Pull request overview

This PR improves OAuth failure UX by redirecting OAuth callback errors to the login page (instead of returning raw JSON/500s), and updates the login error i18n lookup to correctly resolve camelCase error keys like oauthSignin. It also adds backend unit tests and a Cypress spec to cover key redirect/error-display paths.

Changes:

  • Backend: Redirect more OAuth callback failure paths to /login?error=oauthSignin instead of raising HTTPException (JSON) or returning 500s.
  • Frontend: i18n lookup now tries the exact error key before attempting a lowercased fallback.
  • Tests: Adds backend unit tests and a Cypress E2E spec for OAuth error redirects and friendly messaging.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
backend/chainlit/server.py Redirects additional OAuth failure paths to the login page instead of JSON/500 responses.
backend/tests/test_server.py Adds regression tests for OAuth callback redirect behavior (including Azure hybrid flow).
frontend/src/components/LoginForm.tsx Improves translation-key resolution for OAuth error query params (camelCase keys).
cypress/e2e/oauth_auth/spec.cy.ts Adds E2E coverage for redirect-on-error and user-friendly login error message rendering.
cypress/e2e/oauth_auth/main.py Provides a Chainlit app entrypoint configuring OAuth env vars for the new Cypress spec.

Comment thread backend/chainlit/server.py Outdated
Comment thread backend/chainlit/server.py
Comment thread backend/chainlit/server.py
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py
Comment thread backend/tests/test_server.py Outdated
Comment thread cypress/e2e/oauth_auth/spec.cy.ts Outdated

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.

Comment thread frontend/src/components/Alert.tsx
Comment thread backend/chainlit/server.py

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 9 comments.

Comment thread cypress/e2e/oauth_auth/spec.cy.ts
Comment thread cypress/e2e/oauth_auth/spec.cy.ts
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
Comment thread backend/tests/test_server.py Outdated
@dokterbob

Copy link
Copy Markdown
Collaborator Author

The Windows e2e shards are red ("Cypress binary is missing" on windows-latest-5, fail-fast cancellation on the other four), but this is pre-existing, repo-wide infrastructure — not introduced by this PR. The identical failure signature appears on the unrelated feat/compact-cot-display branch (run 27305937906, 2026-06-10). Ubuntu shards are green, confirming the OAuth spec itself is correct.

Root cause and proposed fixes are tracked in #2957. This PR can be reviewed and merged independently of that infra issue.

@postoso

postoso commented Jun 22, 2026

Copy link
Copy Markdown

Nice, this is a solid fix for #1273. OAuth failures redirect back to /login instead of dumping raw JSON now, and resolving the camelCase oauthSignin key via the lookup change is clean.

One thing though: I don't think it fully closes #2956. access_denied currently maps to oauthSignin along with the other failures, so someone who cancels still gets "Sign in failed. Please try again," which is the exact case #2956 wanted to special-case (telling a user who deliberately backed out to "try again" can loop them). All the redirect work here could stay as-is, just route access_denied to its own key.

Happy to put up a small follow-up for that on top of this if you'd want it.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

This PR is stale because it has been open for 14 days with no activity.

@github-actions github-actions Bot added stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed and removed stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed labels Jul 7, 2026
@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 14 days with no activity.

@github-actions github-actions Bot added the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Jul 25, 2026
@dokterbob
dokterbob marked this pull request as ready for review July 29, 2026 14:11
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. auth Pertaining to authentication. backend Pertains to the Python backend. bug Something isn't working e2e-tests Has E2E tests frontend Pertains to the frontend. translation Translation or localisation of strings. labels Jul 29, 2026
@dosubot dosubot Bot added the unit-tests Has unit tests. label Jul 29, 2026
@dokterbob
dokterbob enabled auto-merge July 29, 2026 14:12
@dokterbob

Copy link
Copy Markdown
Collaborator Author

@hayescode @sandangel @asvishnyakov poke

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/chainlit/translations/gu.json Outdated
Comment thread backend/chainlit/server.py
@dokterbob

Copy link
Copy Markdown
Collaborator Author

🤖 Posting as @dokterbob's agent (Claude Code) — @dokterbob reviewed and approved this reply, and will handle the actual review of your PR.

@postoso You're right, and yes please — take it.

Also: closing #2970 as a duplicate of this PR was the wrong call on our side. It wasn't a dup. #2970 added the translation key across all 23 locales but didn't touch server.py, so nothing ever routed access_denied to it — and this PR maps every provider error to oauthSignin, including a deliberate cancel. Neither half covers #2956 on its own. Sorry for the round trip.

Four things worth knowing before you start, so this lands complete:

1. Your #2970 strings are good — carry them over, don't rewrite. The en-US one in particular:

"Sign in was cancelled or access was denied. If this was a mistake, please try again."

2. Rename the key to oauthAccessDenied. #2970 used the raw provider code access_denied as the key; #2956 proposed the camelCase form, which is the one to use. Two reasons: it matches the sibling keys (oauthSignin, oauthAccountNotLinked, oauthCreateAccount), and it keeps raw provider codes out of the redirect URL — this PR deliberately stopped forwarding the provider's raw error value into the query string after Copilot flagged it in review. So the server should redirect to ?error=oauthAccessDenied, not ?error=access_denied.

3. Base it on fix/oauth-error-ux-1273, not main. #2970 targeted main. This PR is still unmerged, and the follow-up depends on both its _get_oauth_redirect_error helper and its raw-key-first i18n lookup.

4. File checklist — the first item is the piece #2970 was missing:

  • backend/chainlit/server.py — an if error == "access_denied" check ahead of the generic fallback in both handlers: oauth_callback (302) and oauth_azure_hf_callback (303). Easy to fix one and miss the other.
  • all 23 files in backend/chainlit/translations/ — key parity is enforced by lint_translation_json (backend/chainlit/translations.py), so a partial set fails.
  • backend/tests/test_server.pytest_oauth_callback_provider_error_redirects currently uses error=access_denied and asserts oauthSignin, so it flips to oauthAccessDenied. Worth adding a case alongside it proving a different provider code still maps to oauthSignin, so the generic path stays covered.
  • cypress/e2e/oauth_auth/spec.cy.ts — same flip, plus a render assertion at /login?error=oauthAccessDenied.

No frontend change needed: LoginForm.tsx already tries the raw key first and falls back through auth.login.errors.default, so apps running an older copied .chainlit/translations/ degrade to "Unable to sign in" rather than breaking.

Ping us when it's up and @dokterbob will review.

@dokterbob

Copy link
Copy Markdown
Collaborator Author

🤖 @dokterbob's agent (Claude Code) again — correcting one thing I got wrong above.

@postoso One correction to my checklist, because it affects how you verify your work.

I wrote that key parity across backend/chainlit/translations/ is "enforced by lint_translation_json, so a partial set fails." It isn't, and it won't.

chainlit lint-translations doesn't check this repo's translation files at all. lint_translations() (backend/chainlit/config.py:692) loads the package's en-US.json as ground truth and then iterates config_translation_dir — the consuming app's .chainlit/translations/ directory, which is gitignored. Run inside a checkout it lints whatever stale copy happens to be sitting there, not the source tree. backend/tests/test_translations.py only exercises compare_json_structures against synthetic dicts, and no CI workflow references translations.

Practical consequence for you: if you add oauthAccessDenied to only some of the 23 files, CI will go green anyway. Nothing will tell you. Which is worth knowing given that a partial change is what made #2970 land incomplete the first time — I don't want to send you into the same trap from the other direction.

Until there's a real check, this is the way to verify locally:

import json, os
from chainlit.translations import compare_json_structures

d = "backend/chainlit/translations"
truth = json.load(open(os.path.join(d, "en-US.json"), encoding="utf-8"))
for f in sorted(os.listdir(d)):
    if f.endswith(".json"):
        for e in compare_json_structures(truth, json.load(open(os.path.join(d, f), encoding="utf-8"))):
            print(f, e)

Silent output means you got all 23.

Heads up that this currently prints six pre-existing gaps unrelated to your change (ar-SA, da-DK, de-DE, it, ja, ko) — those are tracked separately in #2993, along with adding the CI check that should have caught them. Not yours to fix here; just don't be alarmed by the noise.

dokterbob and others added 6 commits July 30, 2026 10:59
… JSON (#1273)

All user-facing failure paths in oauth_callback and oauth_azure_hf_callback
now redirect to /login?error=oauthSignin instead of returning raw JSON or a
bare 500. Developer-facing config errors (missing oauth_callback, unknown
provider) remain HTTPException. The frontend i18n lookup now tries the raw
error key before lowercasing so camelCase keys (oauthSignin) resolve to their
specific messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…1273)

- Rework `_get_oauth_redirect_error` to accept a `status_code` param
  (default 302); POST azure-hybrid handler now uses 303 (See Other) so
  the browser issues a GET to /login instead of re-POSTing.
- Map all provider-returned OAuth errors (including access_denied) to the
  friendly `oauthSignin` key; raw provider code is logged for debugging.
- Add `asyncio.CancelledError: raise` guard before the broad
  `except Exception` in both OAuth callback handlers.
- Reword `oauthSignin` across all 23 locale files:
  "Sign in failed. Please try again, or use a different sign-in method."
- Add `role="alert"` to `Alert.tsx` (ARIA + stable Cypress selector).
- Fix Cypress `oauth_auth/main.py`: remove unused `# noqa: E402` that
  caused RUF100 lint failure in CI.
- Update `spec.cy.ts`: assert new message text, fix ineffective body
  assertion (use content-type check), assert provider-error redirects to
  `error=oauthSignin`.
- Tighten `test_server.py` redirect-status assertions to exact codes
  (302 for GET handler, 303 for POST handler) and check `oauthSignin`
  key for provider-error case.
- Follow-up issue for access_denied-specific UX: #2956.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n log to warning

- Alert: apply role="alert" only for error variant; info uses role="status"
  (assertive announcement is appropriate only for errors, not notices)
- oauth_callback: demote logger.exception → logger.warning for invalid state
  cookie (expected user-facing failure, not an application error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…apture

logger.warning with exc_info=True keeps the traceback (needed by Sentry)
while logging at WARNING rather than ERROR level, since an invalid OAuth
state cookie is an expected user-facing event, not an application error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Signin key

All redirect location assertions now check for `error=oauthSignin` rather
than the generic `/login?error=`, and Cypress status assertions use exact
302 instead of oneOf([3xx]). This prevents regressions in error-key mapping
that a looser check would miss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AzureADHybridOAuthProvider sets response_mode=form_post, so the provider
POSTs error responses as form fields. `error` was declared as a bare
Optional[str], which FastAPI binds as a query parameter, so it never
populated and the provider-error warning added in this PR never fired for
the hybrid flow. Accept both sources rather than swapping query for form,
so any existing query-param caller keeps working.

The redirect was already correct (303 -> /login?error=oauthSignin); this
restores the diagnostic log line, so the new test asserts on the warning
rather than the redirect.

Also adds the missing -no postposition to the Gujarati oauthSignin string,
matching the existing house style at chat.favorites.use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dokterbob
dokterbob force-pushed the fix/oauth-error-ux-1273 branch from 8cf5a15 to 1ba0eb4 Compare July 30, 2026 09:59
@dokterbob dokterbob added review-me Ready for review! and removed stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed labels Jul 30, 2026
@postoso

postoso commented Aug 3, 2026

Copy link
Copy Markdown

Follow-up is up at #3010. It targets this branch, carries over the 23 strings from #2970, handles both callback variants, and keeps other provider errors on oauthSignin. Backend and Cypress coverage are green.

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

Labels

auth Pertaining to authentication. backend Pertains to the Python backend. bug Something isn't working e2e-tests Has E2E tests frontend Pertains to the frontend. review-me Ready for review! size:M This PR changes 30-99 lines, ignoring generated files. translation Translation or localisation of strings. unit-tests Has unit tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UX bug: raw JSON on OAuth errors / Exception handling in auth

3 participants