feat: surface Google SSO on the login page - #83
Conversation
Google Workspace SSO is fully implemented server-side — /auth/google/start and /auth/google/callback exist, are permitAll in SecurityConfig, and are gated by a google_workspace_domain allowlist that verifies the OIDC `hd` claim. The client helper authAPI.getGoogleStartUrl() exists too. Nothing ever called it. Login.jsx had no Google reference at all, so on an install with SECURITY_GOOGLE_ENABLED=true the only way to sign in via SSO was to type /api/auth/google/start into the address bar. The login page is unauthenticated, so it cannot read security.google.enabled to decide whether to offer the button. Rather than add an endpoint, this extends GET /setup/status — already public, and already fetched by Login.jsx on mount — with a googleEnabled flag. Installs that never configured Google see no button and are unaffected. - SetupController: expose googleEnabled on the public status response. Field is non-final by necessity; @requiredargsconstructor would otherwise pull it into the constructor, which Spring cannot satisfy. - Login.jsx: render a "Sign in with Google" button below the password form when the flag is set. Plain <a>, not a submit button — the endpoint 302s to Google, so it is a full-page navigation and must not post the form.
|
Added a second commit: The original PR note flagged that enabling Google does not close
Notable details:
Verified on the same live self-host install, with Login page renders the Google button alone — no password fields, no orphaned "or" divider. With the flag unset, the page is unchanged. |
Follow-up to #83, which noted this gap but did not close it. ## Problem Enabling Google SSO does not disable password sign-in, and there is no way to make it exclusive. - `PasswordlessAuthService.loginWithPassword()` never consults `security.google.enabled` — no branch, no guard - `/auth/login` is `permitAll` in `SecurityConfig` unconditionally - no `security.password.enabled` (or equivalent) exists anywhere So a deployment that puts every human behind Google Workspace — domain-allowlisted, `hd`-claim verified — still leaves `/auth/login` open to every local account. Anyone reasoning "we turned on SSO, so password login is closed" is wrong, and nothing in the codebase says otherwise. This is most acute right after enabling SSO on an install that has seed or demo accounts: those credentials keep working, internet-facing, with SSO fully configured. ## Change Adds `security.password.enabled`, **defaulting to `true`** so existing installs behave exactly as before. **`PasswordlessAuthService`** - Rejects **before** the rate limiter and before any credential comparison. Nothing needs rate-limiting when the path is closed, and rejecting early avoids leaking whether an account exists. - Emits `PASSWORD_LOGIN_FAILURE` with `reason=password_login_disabled`, so refusals are auditable instead of silent. - `@PostConstruct` guard logs an **ERROR** when password *and* Google are both disabled. That combination leaves nobody able to sign in and is otherwise only discovered at the login screen. - The class needed `@Slf4j` — it had no logger. **`SetupController`** — exposes `passwordLoginEnabled` on the public `/setup/status`, alongside the `googleEnabled` added in #83. **`Login.jsx`** — hides the password form, drops the now-meaningless "or" divider, and rewords the subtitle to "Sign in with your work Google account." The frontend guard is `passwordLoginEnabled !== false`, deliberately **not** a truthiness test: `setupStatus` is `null` on first paint and the field is `undefined` on installs predating this flag. Both must render the form. Inverting it would strand users on a login page with no way in whenever the status call was slow or failed. ## Testing Verified on a live self-host install with `SECURITY_PASSWORD_ENABLED=false`: ``` /api/setup/status -> "googleEnabled":true,"passwordLoginEnabled":false POST /api/auth/login -> {"message":"Password sign-in is disabled. Please sign in with Google."} (real ADMIN account, account_status=ACTIVE — a valid credential is refused, not just a bad one) /api/auth/google/start -> 302 (unaffected) startup log -> Password sign-in is DISABLED (security.password.enabled=false); Google SSO only. security_event -> PASSWORD_LOGIN_FAILURE | {"reason": "password_login_disabled"} ``` Login page renders the Google button alone — no password fields, no orphaned divider. With the flag unset, the page and the login behaviour are unchanged.
Problem
Google Workspace SSO is fully implemented server-side but unreachable from the UI.
Present and working:
GET /auth/google/startand/auth/google/callback(AuthController)permitAllinSecurityConfigPasswordlessAuthService.isAllowedWorkspaceDomain()gates on thegoogle_workspace_domainallowlist and a matching OIDChdclaimauthAPI.getGoogleStartUrl()insrc/lib/api/client.jsNothing ever calls that helper.
Login.jsxcontained no Google reference at all, so on an install withSECURITY_GOOGLE_ENABLED=truethe only way to use SSO was typing/api/auth/google/startinto the address bar.Why extend
/setup/statusinstead of adding an endpointThe login page is unauthenticated, so it cannot read
security.google.enabledto decide whether to render the button.GET /setup/statusis already public and already fetched byLogin.jsxon mount, so the flag rides along at no extra request and no new surface area.Installs that never configured Google get
googleEnabled: falseand render exactly as before.Changes
SetupController— exposegoogleEnabledon the public status response.The field is deliberately not
final:@RequiredArgsConstructorwould pull a final field into the generated constructor, and Spring has no bean to satisfy aboolean. Field injection via@Valueis required here.Login.jsx— render "Sign in with Google" below the password form when the flag is set.It is a plain
<a>, not a<button>:/api/auth/google/startresponds302to Google, so this is a full-page navigation and must not submit the password form it sits under. Styling follows the existing form (white/bordered against the dark primary), with an "or" divider.Testing
Verified on a live self-host install (backend 8.4 stack,
SECURITY_GOOGLE_ENABLED=true, Workspace domain allowlisted):GET /api/setup/status→{"setupComplete":true,...,"googleEnabled":true}/loginand navigates toaccounts.google.comscope=openid email profile, PKCEcode_challenge(S256), and a CSRFstateNote for maintainers
The dead
getGoogleStartUrl()helper suggests this was started and never finished. Worth confirming there wasn't a different intended UX (e.g. SSO-only mode hiding the password form) — happy to adjust.Related:
security.password.enableddoes not exist, so enabling SSO does not disable password login. Any deployment relying on SSO as an exclusive gate still has/auth/loginopen to existing local accounts. Not addressed here, but worth flagging.