Skip to content

feat: surface Google SSO on the login page - #83

Merged
geekypunk merged 1 commit into
mainfrom
feat/google-sso-login-button
Aug 24, 2026
Merged

feat: surface Google SSO on the login page#83
geekypunk merged 1 commit into
mainfrom
feat/google-sso-login-button

Conversation

@geekypunk

Copy link
Copy Markdown
Contributor

Problem

Google Workspace SSO is fully implemented server-side but unreachable from the UI.

Present and working:

  • GET /auth/google/start and /auth/google/callback (AuthController)
  • both permitAll in SecurityConfig
  • PasswordlessAuthService.isAllowedWorkspaceDomain() gates on the google_workspace_domain allowlist and a matching OIDC hd claim
  • authAPI.getGoogleStartUrl() in src/lib/api/client.js

Nothing ever calls that helper. Login.jsx contained no Google reference at all, so on an install with SECURITY_GOOGLE_ENABLED=true the only way to use SSO was typing /api/auth/google/start into the address bar.

Why extend /setup/status instead of adding an endpoint

The login page is unauthenticated, so it cannot read security.google.enabled to decide whether to render the button. GET /setup/status is already public and already fetched by Login.jsx on mount, so the flag rides along at no extra request and no new surface area.

Installs that never configured Google get googleEnabled: false and render exactly as before.

Changes

SetupController — expose googleEnabled on the public status response.

The field is deliberately not final: @RequiredArgsConstructor would pull a final field into the generated constructor, and Spring has no bean to satisfy a boolean. Field injection via @Value is 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/start responds 302 to 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}
  • button renders on /login and navigates to accounts.google.com
  • the redirect carries scope=openid email profile, PKCE code_challenge (S256), and a CSRF state
  • with the flag unset, no button renders and the page is byte-identical to before

Note 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.enabled does not exist, so enabling SSO does not disable password login. Any deployment relying on SSO as an exclusive gate still has /auth/login open to existing local accounts. Not addressed here, but worth flagging.

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.
@geekypunk
geekypunk requested a review from a team as a code owner August 24, 2026 19:09
@geekypunk
geekypunk merged commit 8081bb4 into main Aug 24, 2026
9 checks passed
@geekypunk
geekypunk deleted the feat/google-sso-login-button branch August 24, 2026 20:14
@geekypunk

Copy link
Copy Markdown
Contributor Author

Added a second commit: security.password.enabled, so SSO can actually be exclusive.

The original PR note flagged that enabling Google does not close /auth/login. This implements the missing toggle rather than leaving it as a caveat.

loginWithPassword() never consulted security.google.enabled, /auth/login is permitAll unconditionally, and no flag existed — so a deployment fronting DeepSQL with Google Workspace still had every local account reachable by password. The new flag defaults to true, so existing installs are unaffected.

Notable details:

  • The check sits 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.
  • Refusals emit PASSWORD_LOGIN_FAILURE with reason=password_login_disabled, so turning it on is auditable rather than silent.
  • A @PostConstruct guard logs an ERROR when password and Google are both disabled — that combination locks everyone out and is otherwise only discovered at the login screen.
  • The frontend guard is passwordLoginEnabled !== false, not a truthiness test. setupStatus is null on first paint and the field is undefined on installs predating the flag; both must render the form. Inverting that would strand users on a login page with no way in whenever the status call was slow or failed.

Verified on the same live self-host install, with SECURITY_PASSWORD_ENABLED=false:

/api/setup/status  -> "googleEnabled":true,"passwordLoginEnabled":false
POST /api/auth/login (real admin, valid account)
                   -> {"message":"Password sign-in is disabled. Please sign in with Google."}
/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 "or" divider. With the flag unset, the page is unchanged.

geekypunk added a commit that referenced this pull request Aug 24, 2026
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.
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