Skip to content

🪪 feat: Add Google OAuth Login to Admin Panel#36

Open
dustinhealy wants to merge 9 commits into
mainfrom
feat/admin-google-oauth
Open

🪪 feat: Add Google OAuth Login to Admin Panel#36
dustinhealy wants to merge 9 commits into
mainfrom
feat/admin-google-oauth

Conversation

@dustinhealy

@dustinhealy dustinhealy commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a "Continue with Google" button to the admin login page alongside the existing OpenID flow. LibreChat's /api/admin/oauth/google and /api/admin/oauth/google/callback routes were already wired, we just needed to consume them in the admin panel.

The provider availability layer was rewritten while in the area: instead of the openid-only GET /api/admin/oauth/openid/check probe, the loader now reads LibreChat's existing /api/config startup payload (the same endpoint LibreChat's chat client uses) and derives a provider availability map from *LoginEnabled flags. This eliminates the need for per-provider check endpoints, and makes adding future providers (GitHub, Discord, SAML) a one-line registry append.

Provider registry:

  • src/constants/oauth.ts — single registry entry per provider (startPath, callbackRoute, enabledKey, optional labelKey/imageKey for OIDC/SAML branding overrides, optional click-ui Logo name).
  • OAuthProvider union widened from 'openid' to 'openid' | 'google'; same shape on SessionData.tokenProvider.

Server functions (src/server/auth.ts):

  • Replaced checkOpenIdFn / openIdCheckOptions with getStartupConfigFn / startupConfigOptions which fetches /api/config once and returns { providers, ssoOnly }.
  • Replaced openidLoginFn with provider-parameterized oauthLoginFn (PKCE generation centralized in buildOAuthLoginUrl).
  • oauthExchangeFn now takes { code, provider }; the upstream wire body to /api/admin/oauth/exchange is unchanged (still { code, code_verifier }) — provider is admin-panel-internal and only drives the session's tokenProvider field.
  • Refresh-token forwarding (refreshAdminToken) is openid-only because LibreChat's admin OAuth exchange only exposes a refresh token for OpenID. Google admin sessions arrive without one: the googleAdmin passport strategy at api/strategies/googleStrategy.js does not pass access_type=offline, and createOAuthHandler at api/server/controllers/auth/oauth.js:46-50 only forwards refresh tokens when provider === 'openid' && OPENID_REUSE_TOKENS=true. As a result, Google admin users are re-prompted at JWT expiry (~15 min default). Tracking the upstream LibreChat change to capture and expose a Google admin refresh token as a follow-up; the admin panel side is ready to consume one as soon as it ships.

UI (src/components/AuthCard.tsx, src/routes/login.tsx):

  • Single SSO button replaced with one button per enabled provider, rendered in registry order. Branded providers (Google) render the click-ui Logo glyph; OIDC consumes openidLabel / openidImageUrl from /api/config when set by the deployer.
  • ADMIN_SSO_ONLY semantics extended for the multi-provider world: hides the password form whenever any SSO provider is enabled; auto-redirects only when exactly one provider is configured. Per product requirement, ssoOnly with multiple providers shows all buttons with no auto-redirect.
  • Login route loader fans out to a single /api/config fetch on the SSR boundary — no client-visible discovery roundtrip.

Routes:

  • New src/routes/auth/google/callback.tsx mirroring the openid callback; passes provider: 'google' to oauthExchangeFn.
  • OpenID callback updated to pass provider: 'openid' for symmetry.

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

Manual end-to-end (real Google OAuth client):

  1. Created a Google Cloud OAuth 2.0 Web Client with redirect URIs for /oauth/google/callback (LibreChat) and /api/admin/oauth/google/callback (admin panel).
  2. Configured LibreChat with GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / ALLOW_SOCIAL_LOGIN=true.
  3. Verified GET /api/config returns googleLoginEnabled: true.
  4. Promoted the test Gmail account to ADMIN in MongoDB (admin Google route requires an existing admin user; ALLOW_SOCIAL_REGISTRATION only applies to the chat path).
  5. Clicked "Continue with Google" on /login → Google consent → exchange succeeded → admin dashboard rendered, tokenProvider: 'google' set in the encrypted admin session.
  6. Regression-checked: email/password login, OIDC login, and 2FA flow all still work.
Screenshot 2026-04-26 at 1 54 57 PM

Automated:

  • e2e/login.spec.ts — added "shows Google button alongside OpenID when both are configured" assertion. Existing OpenID/SSO/2FA tests adapted to the new button labels (Continue with OpenID / Continue with Google).
  • e2e/mock-backend.mjs — added GET /api/config handler returning both openidLoginEnabled: true and googleLoginEnabled: true so multi-provider rendering exercises in CI.
  • e2e/auth.setup.ts — fixed pre-existing Sign In / Sign in casing mismatch that was blocking the suite locally.

Test Configuration

  • LibreChat dev branch (last sync b632367c8)
  • Admin panel feat/admin-google-oauth (this branch)
  • Local mongodb

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

Note

High Risk
Touches admin authentication (OAuth discovery, PKCE, session tokenProvider, SSO-only policy); Google sessions may lack refresh tokens until upstream LibreChat changes.

Overview
Adds Google (“Continue with Google”) on the admin login page and refactors SSO from a single OpenID probe to multi-provider flows driven by LibreChat’s public /api/config payload.

Discovery & server: checkOpenIdFn / openidLoginFn are replaced by getStartupConfigFn (returns { providers, ssoOnly } from *LoginEnabled flags, respects socialLoginEnabled for Google, forwards X-Tenant-Id) and oauthLoginFn / oauthExchangeFn with a provider argument (openid | google). Provider metadata lives in src/constants/oauth.ts.

UI: AuthCard renders one OAuthButton per enabled provider (password form + separator when allowed). ADMIN_SSO_ONLY hides password login even if no providers resolve, with a warning when misconfigured; auto-redirect runs only when SSO-only and exactly one provider is available.

Routes & tests: New /auth/google/callback; OpenID/Google callbacks pass provider and surface upstream error / error_description. E2E mock serves GET /api/config; Playwright targets Sign in and OpenID/Google button labels.

Reviewed by Cursor Bugbot for commit 8463b66. Bugbot is set up for automated code reviews on this repo. Configure here.

@dustinhealy dustinhealy marked this pull request as ready for review April 26, 2026 01:41
@dustinhealy dustinhealy self-assigned this Apr 26, 2026
@dustinhealy dustinhealy requested a review from danny-avila April 26, 2026 01:41
@danny-avila

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ 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 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.

Thanks for this, @dustinhealy — the feature itself is solid and the backend supports it (the admin Google routes exist in LibreChat: api/server/routes/admin/auth.js /oauth/google and /oauth/google/callback, backed by the googleAdmin passport strategy). Requesting changes purely on mergeability/freshness, not on the approach.

Blocking: branch is conflicting and stale

The PR currently reports mergeable: CONFLICTING / DIRTY against main, and it predates the OpenID/admin-auth refactors that have since landed:

  • #46 — body-based OpenID refresh for cross-origin admin panels
  • #50 — OpenID redirect origin handling + startup logs
  • #59 — guard admin SSO PKCE verifier loss

Those reworked the same files this PR touches, so it needs a real rebase before it can be reviewed/merged. Expect to re-reconcile at least:

  • src/server/auth.ts — the OAuth init/exchange + PKCE/origin handling has changed substantially; the new Google flow should reuse the current openidLoginFn/oauthExchangeFn patterns (PKCE code_challenge, origin binding, session codeVerifier) rather than the older shape this branch was built on.
  • src/components/AuthCard.tsx and src/routes/login.tsx — SSO button/auto-redirect wiring has moved.
  • src/routeTree.gen.ts — regenerate (don't hand-merge) after adding src/routes/auth/google/callback.tsx.
  • src/types/auth.ts, src/types/server.ts, src/constants/oauth.ts, locales — re-check against current definitions.

Requested changes

  1. Rebase onto latest main and resolve conflicts; regenerate routeTree.gen.ts rather than resolving it by hand.
  2. After rebase, align the Google callback/exchange with the current admin OAuth contract (PKCE + origin binding via request headers; the backend strips redirect_uri/code_challenge/redirectTo, see packages/api/src/auth/exchange.ts).
  3. Confirm tsc --noEmit and vitest run are green post-rebase, and that the e2e mock-backend updates still reflect the current exchange shape.

Happy to re-review as soon as it's rebased and green. Thanks again!

Renders a "Continue with Google" button on the admin login page alongside
the existing OpenID flow, fully delegating to LibreChat's already-wired
/api/admin/oauth/google routes (no upstream changes needed).

Provider availability is now sourced from LibreChat's public /api/config
startup payload — the same endpoint LibreChat's own client uses — instead
of the openid-only /admin/oauth/openid/check endpoint. This makes the
admin panel pick up github/discord/saml automatically once they're added
to the registry, with no per-provider check endpoints required.

Detection layer landed as a small src/constants/oauth.ts registry plus a
generalized oauthLoginFn / oauthExchangeFn pair parameterized by provider.
The exchange wire body to LibreChat is unchanged; provider tagging is
admin-panel-internal and drives session.tokenProvider for telemetry.

Refresh-token cookie forwarding stays openid-only because LibreChat's
/api/auth/refresh only branches on token_provider=openid (and only when
OPENID_REUSE_TOKENS is set). Google sessions correctly fall through the
default JWT refresh path.

ADMIN_SSO_ONLY semantics extended for the multi-provider world: hides the
password form whenever any SSO provider is enabled, auto-redirects only
when exactly one provider is configured.
@dustinhealy dustinhealy force-pushed the feat/admin-google-oauth branch from bc620cb to 9ab4f6a Compare June 17, 2026 17:32
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

window.location.href = result.authUrl;

P2 Badge Preserve the requested redirect for manual SSO clicks

When a user is sent to /login?redirect=/some/page and manually clicks an OAuth provider button, this branch navigates to the provider URL without adding the redirectTo parameter that the auto-redirect branch adds above. As a result the callback can only redirect to /, so Google SSO users who were bounced from a protected route land on the dashboard instead of the page they originally requested; build a URL here and append redirectTo before assigning window.location.href.

ℹ️ 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".

Comment thread src/routes/auth/google/callback.tsx Outdated
Comment thread e2e/mock-backend.mjs
The inline provider button rendering in AuthCard branched on the presence
of a logo glyph: branded providers needed children for the logo+text
composition (and an eslint-disable for click-ui's button-requires-label
rule), while unbranded providers used the label prop directly. Both paths
shared identical type, disabled, onClick, and fillWidth wiring.

Moving the branch into a dedicated OAuthButton scopes the eslint-disable
to a single helper, collapses the provider-rendering site in AuthCard to
a plain map, and gives the future github/discord/saml additions a single
file to land their glyph rules in.
The Google callback's error-state retry was rendered as a bare anchor to
/login, which bypasses the router's basepath. Under VITE_BASE_PATH=/admin
that anchor navigates to the domain root and lands outside the mounted
admin panel, where it 404s. The OpenID callback already uses TanStack
Link for the same path; routing the Google callback through Link too
keeps both flows aligned and basepath-correct.
The webServer readiness URL still targeted the removed
/api/admin/oauth/openid/check endpoint, so Playwright spent the
configured 10s timeout polling a 404 before declaring the mock backend
ready. /api/config is the live provider-discovery endpoint the admin
panel now reads on the SSR login boundary and is served from the same
mock process, so it doubles as a real liveness signal.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bde4844072

ℹ️ 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".

Comment thread src/server/auth.ts
Comment thread src/server/auth.ts
Comment thread src/routes/auth/google/callback.tsx
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

window.location.href = result.authUrl;

P3 Badge Preserve redirect targets for provider button clicks

When a user is sent to /login?redirect=... and manually clicks Google/OpenID, this branch assigns the returned auth URL directly, unlike the auto-redirect path above that appends redirectTo. That drops the protected-route destination for all non-auto SSO clicks, so after completing OAuth the user cannot be returned to the page that originally required auth. Mirror the auto-redirect handling before navigating.

ℹ️ 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".

Comment thread src/server/auth.ts
Comment thread src/server/auth.ts
Comment thread src/routes/auth/google/callback.tsx
Comment thread src/server/auth.ts Outdated
`getStartupConfigFn` previously surfaced any provider whose
`*LoginEnabled` flag was true, even when LibreChat had not enabled
social login at all. With `ALLOW_SOCIAL_LOGIN=false` (the default in
LibreChat's `.env.example`) the upstream `configureSocialLogins` never
registers the `googleAdmin` passport strategy, so the admin Google
button auto-redirected into an "Unknown authentication strategy" 500
and `ADMIN_SSO_ONLY=true` deployments hid the password fallback behind
it. The provider registry now carries an opt-in `social` flag, and
`getStartupConfigFn` filters social providers on `socialLoginEnabled`
while leaving openid (its own registration path) unaffected.

The same call now forwards the `X-Tenant-Id` header from the BFF
request to LibreChat's `/api/config`, which is mounted behind
`preAuthTenantMiddleware`. Without it, multi-tenant deployments with
tenant-specific `registration.socialLogins` fell back to the base
config and rendered the wrong provider set for the tenant the user was
actually accessing.
Adds an explicit comment near the session write in `oauthExchangeFn`
documenting why non-openid OAuth admin sessions arrive without a
refresh token: LibreChat's `googleAdmin` passport strategy does not
request `access_type=offline`, and `createOAuthHandler` only forwards
refresh tokens when `provider === 'openid' && OPENID_REUSE_TOKENS=true`.

The practical effect is that Google admin users are re-prompted at JWT
expiry. A proper fix lives upstream in LibreChat (capture and expose a
refresh token for Google admin exchanges). Tracking that as a separate
follow-up.
LibreChat's admin OAuth routes redirect passport/PKCE/auth failures
back with `error` and `error_description` query params (e.g.
`pkce_store_failed`, `auth_failed`). The callback loaders previously
accepted only `code` and treated everything else as `invalid_code`,
so a cancelled Google consent or an upstream auth failure surfaced
"Authorization code has expired" instead of the real reason.

Both google and openid callbacks now accept `error` /
`error_description` in their search schemas and render the upstream
description verbatim. Falling back to `error` itself keeps the page
useful when the upstream redirect omits the description.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f4eb8be. Configure here.

Comment thread src/components/AuthCard.tsx Outdated
`hidePasswordForm` previously included a `providers.length > 0` clause
as a defensive fallback, but combined with the new social-login gate
that can legitimately leave `providers` empty for an
`ADMIN_SSO_ONLY=true` deployment (e.g. only Google configured but
upstream `ALLOW_SOCIAL_LOGIN=false`), it leaked the password form back
into the page and defeated the deployer's SSO-only intent.

`hidePasswordForm` now collapses to `ssoOnly`. When `ssoOnly` is set
and discovery returns no providers, `AuthCard` shows a warning banner
via a new `com_auth_sso_required_unconfigured` locale key so the
misconfigured state is visible instead of silently degrading.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 115433a9c4

ℹ️ 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".

Comment thread src/server/auth.ts
Comment thread src/routes/auth/google/callback.tsx
Comment thread src/routes/auth/google/callback.tsx
The Bun server's metrics wrapper normalizes any path missing from
`KNOWN_APP_ROUTES` to `unknown`, so the new Google admin callback was
collapsing every login attempt (success or failure) into the same bucket
as bot probes and 404s. Adds `/auth/google/callback` to the registry next
to the existing openid entry, and to the metrics test matrix so the
mapping is locked in.
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

1 similar comment
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if (result.authUrl) {
window.location.href = result.authUrl;

P2 Badge Preserve redirects through manual SSO

When an admin is sent to /login?redirect=/grants and has to choose among multiple SSO providers (for example ADMIN_SSO_ONLY=true with OpenID and Google), this manual provider path sends the raw auth URL without carrying redirectTo, while the callback routes redirect to / after exchange. The result is that successful Google/OpenID SSO from a protected page lands on the dashboard instead of the originally requested route; mirror the auto-redirect path's redirect preservation or persist the redirect through the exchange.

ℹ️ 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".

Comment thread src/server/auth.ts
Comment thread src/components/AuthCard.tsx
@dustinhealy

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8463b6677a

ℹ️ 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".

Comment thread src/server/auth.ts
Comment on lines +358 to +360
for (const def of OAUTH_PROVIDERS) {
if (config[def.enabledKey as keyof t.StartupConfigResponse] !== true) continue;
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect socialLogins when resolving providers

When a deployment leaves Google credentials set but removes google from LibreChat's registration.socialLogins for a tenant/base config, /api/config can still have googleLoginEnabled: true; this loop only checks the env-derived enabled flag, so the admin panel renders and starts a Google login that LibreChat's startup config intentionally hides. Filter against the socialLogins list from /api/config before pushing social providers.

Useful? React with 👍 / 👎.

Comment thread src/server/auth.ts
Comment on lines +358 to +360
for (const def of OAUTH_PROVIDERS) {
if (config[def.enabledKey as keyof t.StartupConfigResponse] !== true) continue;
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate OpenID before advertising it

For OpenID deployments with the required env vars present but a bad issuer/discovery setup, /api/config can still report openidLoginEnabled: true; this now adds the OpenID provider without exercising LibreChat's admin OpenID check, so the login page (or SSO-only auto-redirect) sends admins into a failing OAuth flow that the previous /api/admin/oauth/openid/check probe would have hidden. Keep the admin check for OpenID or validate the provider before pushing it here.

Useful? React with 👍 / 👎.

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.

2 participants