Skip to content

⏳ fix: Restore the Sign-In Card Width and Keep SSO Buttons Busy Until the Provider Loads - #196

Merged
danny-avila merged 5 commits into
mainfrom
pr/redo-login-card-sso-loading
Oct 28, 2026
Merged

danny-avila merged 5 commits into
mainfrom
pr/redo-login-card-sso-loading

Conversation

@danny-avila

Copy link
Copy Markdown
Contributor

Pull Request Template

⚠️ Before Submitting a PR, Please Review:

  • Please ensure that you have thoroughly read and understood the Contributing Docs before submitting your Pull Request.

⚠️ Documentation Updates Notice:

  • Kindly note that documentation updates are managed in this repository: librechat.ai

Summary

This PR supersedes #123 by @dustinhealy and redoes it on top of #188. It follows #123's approach: Panel's fillWidth for the card width, click-ui's loading on the SSO button, a busy state that ends only when a start fails, and a pageshow reset for back-forward cache restores. #123 no longer applies as it stands. main already has its click-ui 0.9.1 bump and Vitest config, and #188 replaces the single OpenID button it edits with one button per provider, all started by startSso.

Depends on #188. It edits the AuthCard that #188 rewrites, so its branch is based on #188's. Merge #188 first. This PR's own change is the single commit on top.

The sign-in page had two problems:

  • Narrow card: click-ui 0.9.1 ships unlayered CSS, and .panel { width: var(--panel-width, auto) } outranks Tailwind's layered w-full. Both cards took their content's width. The sign-in card measured 304px at 1440 and 264px at 375, against max-w-md's 448px. The SSO-only redirect card measured 311px and 271px.
  • SSO button gives up before the provider loads: startSso reset its busy state in a finally that runs right after window.location.href = authUrl. Assigning href doesn't wait for the navigation. With LibreChat's start route held for 3 s, the button went back to "Continue with Google", enabled, 1 ms after beforeunload, and stayed that way while the provider loaded, so a second click was possible. It never had a spinner or aria-busy either: it used disabled, not loading.

Changes (src/components/AuthCard.tsx)

  • Card width: both Panels take fillWidth, which click-ui applies as an inline --panel-width: 100%, in place of the w-full that lost, and keep max-w-md. That alone stops the collapse to the content's width: the card spans the page's centered column, up to 448px. A short JSDoc on the class constant says why w-full doesn't work there.
  • Busy until the provider loads: the finally is gone. A new failSso clears the busy state, brings back the password form and shows the error. It runs on an error result, when the request throws, and when the result's URL is empty. ssoStartFn always returns a URL today; the check keeps an empty one from reloading /login. On success, the clicked provider's button stays busy until the provider's page replaces the panel.
  • Spinner and aria-busy: the clicked button takes loading={ssoStarting === id}. click-ui then adds its shimmer, disabled and aria-busy="true", and keeps the "Redirecting…" label. The other providers stay disabled, as before.
  • SSO-only redirect card: with ADMIN_SSO_ONLY and one provider, the busy state starts as that provider, so the redirect card shows from the first render. It shows while that provider is starting and hasn't failed, so the auto-redirect uses the same state and the same error-only reset.
  • Back from the provider: a pageshow listener clears the busy state when event.persisted is true, meaning the browser restored the page from its back-forward cache. The button is idle again. On an SSO-only page, the card offers its provider's button instead of the redirect spinner, which would otherwise spin forever because the auto-redirect has already run.

Not carried over from #123

  • The click-ui 0.9.1 bump and the Vitest server.deps.inline / .claude/** config. main already has both.
  • The vi.mock('@/hooks') with a key-echoing useLocalize, and the stubbed window.location. The new tests use the existing AuthCard.test.tsx setup: real useLocalize with localizeEn, real click-ui, and authUrls that are hashes, so jsdom follows them.
  • min-w-70. With fillWidth the card already spans the page's column, so a min-width only takes effect when it is wider than that column. Its fixed 280px would then push the card plus the page's 16px gutters past a 280px-wide screen.

Unchanged

  • Chromium doesn't keep /login in its back-forward cache. The page is served Cache-Control: no-store, and the start sets a session cookie. Back from the provider reloads the page, so the button is idle. On an SSO-only page the reload redirects to the provider again, as it does on main. The pageshow reset covers browsers that do restore the page.
  • If the navigation stops before the provider answers (Esc, or a 204 from the start route), the page gets no event, so the button stays busy until a reload.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Unit: src/components/__tests__/AuthCard.test.tsx renders the real AuthCard with real click-ui. ssoStartFn is mocked.

  • Pending: while the start is pending, the clicked button is "Redirecting…" with aria-busy="true" and disabled, and the other provider is disabled without aria-busy.
  • Sent to the provider: once the start resolves with a URL and the browser follows it, the button is still busy and disabled.
  • Failures: after an error result, a rejected request or an empty URL, the error shows and the clicked button is idle again (no aria-busy, enabled). On an SSO-only page with two providers, the empty URL also brings back the hidden password form.
  • Back-forward cache: a pageshow with persisted: false leaves the button busy, and one with persisted: true makes it idle with no error.
  • SSO-only: the first render (server-rendered markup, before any effect runs) is the redirect card, with no buttons and no start yet. Once the browser is sent to the provider, the redirect card stays up with no buttons. A persisted pageshow replaces it with an idle provider button, with no password form and no second start.
  • Width: both Panels carry --panel-width: 100% and max-w-md.

Against #188's AuthCard, 7 of the 11 new tests fail. The three failure tests pass on both, because #188 also resets on failure; they fail if failSso stops clearing the busy state. The first-render test also passes on #188, whose redirect card doesn't depend on the busy state; it fails here if the busy state starts empty. I also removed each fix in turn to check which tests catch it. Without the pageshow listener, the two back-forward cache tests fail. Without loading, the pending, sent and back-forward cache tests fail. Without the empty-URL check, its test fails. Without fillWidth, both width tests fail. If the redirect card ignores the busy state, the SSO-only back-forward cache test fails.

E2E (e2e/login.spec.ts, against the mock backend):

  • Card width: the card measures 448px at 1440 and 343px at 375.
  • Busy while leaving: the mock's Google start route is held, and the leaving page logs its buttons 100 ms after beforeunload. Playwright locators and evaluate wait for a pending navigation to finish, so the page logs to the console instead. The test expects "Redirecting…" busy and disabled, OpenID disabled and Sign in untouched, in any order, so it holds whichever order the mock lists its providers in. It then releases the route and checks that the provider's page loads.

All three tests fail against #188's AuthCard: the widths are 304 and 264, and the Google button is "Continue with Google", enabled, without aria-busy. With this change the whole login.spec.ts passes against the mock backend (36 tests).

Live check: against a LibreChat backend with Google admin sign-in (placeholder credentials, the provider page stubbed), and the production build, in Chromium.

#188 This PR
Sign-in card at 1440 / 375 / 320 / 280 304 / 264 / 264 / 248px 448 / 343 / 288 / 248px, no horizontal scroll
SSO-only redirect card at 1440 / 375 311 / 271px 448 / 343px
Google button, start route held 3 s "Redirecting…" disabled without aria-busy, then "Continue with Google" enabled 1 ms after beforeunload "Redirecting…" with the shimmer, aria-busy and disabled from the click until the provider page loads
Start request fails error shown, button idle error shown, button idle
Provider answers 204 (page stays) button idle button busy; a persisted pageshow makes it idle
Back from the provider page reloads, button idle page reloads, button idle
Back on an SSO-only page reloads and redirects again reloads and redirects again

Light and dark at 1440 look the same apart from the theme.

Test Configuration:

  • LibreChat dev backend with ALLOW_SOCIAL_LOGIN=true and Google client credentials, MongoDB 8.2 replica set. For the SSO-only rows, ADMIN_SSO_ONLY=true on the panel.
  • The panel's production build (bun run build + bun server.ts), Chromium via Playwright.
  • npx playwright test e2e/login.spec.ts against the mock backend.
  • npx tsc --noEmit, npx eslint src/ e2e/ --max-warnings 0, npx prettier --check on the changed files, bun run test:lint-rules, npx vitest run (154 files, 3402 tests), bun run build.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.

… and Keep Deep Links Through SSO

The sign-in page offers one button per SSO provider LibreChat has for admins,
from a registry in src/constants (SSO_PROVIDERS: OpenID, Google, SAML).
ssoProvidersFn replaces checkOpenIdFn and returns { providers, ssoOnly }.
OpenID still needs its /api/admin/oauth/openid/check probe. Google and SAML
come from one GET /api/config sent with the request's tenant, and count only
with socialLoginEnabled, since LibreChat registers their admin strategies under
ALLOW_SOCIAL_LOGIN alone. A provider left out of socialLogins is not offered,
as on LibreChat's own sign-in page. ADMIN_SSO_ENABLED=false still offers none.
OpenID and SAML buttons take the deployer's label from /api/config.

One POST ssoStartFn ({ provider, redirectTo }) builds the PKCE challenge and
keeps the verifier and the sanitized in-app path (toSafeRedirect) in the admin
session, instead of the redirectTo query LibreChat strips. oauthExchangeFn takes
{ code, provider }, stores the provider as tokenProvider, and returns and then
forgets the path. One /auth/$provider/callback route replaces the OpenID-only
one: an unknown provider is not found, a failure shows SsoError, and a sign-in
returns to the page it started from. Both callback paths are known metrics
routes.

Google sessions refresh through POST /api/admin/oauth/refresh with
provider: 'google' and keep the rotated refresh token. SAML sessions, which
LibreChat cannot renew, are never sent to the cookie-based /api/auth/refresh.

ADMIN_SSO_ONLY auto-redirects only when exactly one provider is offered, hides
the password form while any is, and brings the form back when a provider cannot
start or none resolves.

Supersedes #36 by @dustinhealy, reusing its provider registry, its
/api/config-based availability with the social-login and socialLogins gates,
and its idea of carrying the deep link through the admin session.

Closes #30
Closes #65
… the Provider Loads

click-ui 0.9.1's unlayered `.panel { width: var(--panel-width, auto) }`
outranks Tailwind's layered `w-full`, so the sign-in card and the SSO-only
redirect card took their content's width: 304px and 311px at 1440 instead of
max-w-md's 448px. Both Panels now take Panel's `fillWidth` (an inline
`--panel-width: 100%`) and keep max-w-md.

startSso reset its busy state in a `finally` that ran right after
`window.location.href = authUrl`, so the button went back to its idle label,
enabled, while the browser was still loading the provider, and could be
clicked again. Only a start that fails gives the button back now: an error
result, a request that throws, or a result with an empty URL (assigning it
would reload /login). The button takes click-ui's `loading` (shimmer,
disabled, aria-busy); the other providers stay disabled.

On an SSO-only page with one provider, that state starts as the provider, so
the redirect card shows from the first render and ends the same way. A
`pageshow` with `persisted` (the browser restoring the page from
its back-forward cache, Back from the provider) clears it: the button is idle
again, and an SSO-only page offers its provider's button instead of a spinner
that never ends.

Supersedes #123 by @dustinhealy, whose fillWidth, loading and pageshow
approach this follows. Depends on #188.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: 0007c7b

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T09:00:32.356744Z 0007c7b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 0007c7bef1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila changed the base branch from pr/redo-admin-sso-providers to main October 28, 2026 07:15
@danny-avila
danny-avila merged commit a1051c3 into main Oct 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant