You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/GOOGLE_SSO.md

13 KiB

Google SSO

WaboSign supports "Sign in with Google" as an additive authentication path. Once you set the environment variables below, a Google button appears on the sign-in page alongside the existing email-and-password form. Password sign-in keeps working — SSO does not replace it.

This document covers operator setup, runtime behaviour, verification, and troubleshooting.


What you get

  • "Sign in with Google" button on /users/sign_in whenever the env vars are set.
  • Domain-restricted access: only Google accounts whose Workspace hd (hosted-domain) claim matches your allowlist can sign in.
  • Just-in-time (JIT) user provisioning: a first-time Google sign-in from an allowed domain creates a WaboSign user in the default account.
  • 2FA bypass: a user who signed in via Google is not prompted for the WaboSign OTP — Google's MFA is trusted.
  • Password sign-in continues to work for any user that has a password (additive, not replaced).

What you don't get out of the box:

  • No admin UI for credentials — configuration is via env vars only.
  • No SAML/Okta/Entra/Keycloak — Google-only. (See the SSO settings page for hints on adding SAML if you need it later.)
  • No role mapping from Google Workspace groups — every JIT-created user is role: 'admin' (the only role WaboSign ships with).
  • No automatic deprovisioning when a user is removed from your Workspace — manage WaboSign accounts via the existing Users settings page.

Prerequisites

  1. A Google Cloud project with the "Google Identity" / OAuth consent screen configured.
  2. An "OAuth client ID" of type Web application with:
    • Authorized redirect URI: https://<your-host>/users/auth/google_oauth2/callback
    • Authorized JavaScript origins are not required.
  3. A Google Workspace domain whose users should be allowed to sign in (for personal Gmail accounts, see "Open access" below).

Create the OAuth client at https://console.cloud.google.com/apis/credentialsCreate credentials → OAuth client ID. Copy the client ID and client secret to your environment.


Configuration

Set these environment variables on the WaboSign process (in wabosign.env, the docker-compose environment: block, or your hosting provider's secret store):

Variable Required Example Notes
GOOGLE_CLIENT_ID yes 1234.apps.googleusercontent.com From the Google Cloud OAuth client.
GOOGLE_CLIENT_SECRET yes GOCSPX-… From the Google Cloud OAuth client.
GOOGLE_ALLOWED_DOMAINS recommended wabo.cc,partner.example Comma-separated. Only Google accounts whose hd claim is in this list can sign in. Empty = any Google account allowed (a warning is logged at boot).
GOOGLE_DEFAULT_ACCOUNT_ID no 1 The WaboSign Account JIT-provisioned users are attached to. Defaults to the oldest account. Useful only if you run multiple Account records on one deployment.

After changing any of these, restart the WaboSign process — Devise's OmniAuth strategy is registered at boot.

The Devise integration is conditional: if either GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is missing, the :omniauthable Devise module is not loaded and the Google button is hidden. This means development environments without creds keep working unchanged.


Runtime behaviour

Sign-in flow

  1. User clicks Sign in with Google on /users/sign_in.
  2. They are redirected to Google's consent screen. If GOOGLE_ALLOWED_DOMAINS is non-empty, the hd parameter is passed so Google restricts the account chooser at its end too (defense-in-depth).
  3. Google redirects back to /users/auth/google_oauth2/callback. Users::OmniauthCallbacksController#google_oauth2 handles the response:
    • The hd claim is checked against GOOGLE_ALLOWED_DOMAINS. Mismatch → redirect to sign-in with the "not permitted" flash.
    • The email claim is looked up case-insensitively in the users table.
  4. If a matching user exists:
    • If their provider/uid are unset, they get linked to this Google identity and signed in.
    • If they already have a different uid linked, sign-in is rejected (defends against account takeover by email collision).
  5. If no matching user exists, a new one is JIT-provisioned in the default account, with role: 'admin', the user's Google first/last name, a random unused password, and confirmed_at: now.
  6. session[:bypass_otp_for_sso] is set so the post-login MFA-setup redirect in DashboardController#maybe_redirect_mfa_setup is skipped.

2FA interaction

Users who signed in via Google never see the WaboSign OTP prompt — regardless of whether their account has otp_required_for_login: true. The reasoning: Google enforces its own MFA, and a second OTP step would be redundant.

Password users are unaffected — they still see the OTP prompt if they have 2FA enabled.

If you ever sign out and back in via password, the bypass flag is cleared and the normal OTP path applies.

Open access (no allowlist)

Leaving GOOGLE_ALLOWED_DOMAINS empty enables sign-in for any Google account, including personal Gmail. A Rails.logger.warn is emitted at boot:

[Wabosign] Google SSO is enabled but GOOGLE_ALLOWED_DOMAINS is empty — any Google account will be permitted to sign in.

Use this only for demo or single-user deployments. For business deployments, always set an allowlist.


Verification

After deploying:

  1. Status page — open https://your-host/settings/sso as an admin. You should see a green banner: "Google SSO is enabled. Allowed Workspace domain: wabo.cc." If you see "Google SSO is not configured", your env vars didn't reach the process — check your secret loader.

  2. Happy path — open https://your-host/users/sign_in in a private window. Click Sign in with Google. Use a Google account whose domain is on the allowlist. You should land on the WaboSign dashboard signed in. If the user didn't already exist in WaboSign, they were just JIT-created in the default account.

  3. Domain rejection — repeat with a Google account whose domain is not on the allowlist (e.g. a personal @gmail.com). You should be redirected back to /users/sign_in with the flash: "Google sign-in failed: this Google account is not permitted to sign in."

  4. Password still works — sign in as a different user with email + password. The flow should be unchanged from before SSO was enabled (still OTP-gated if that user has 2FA on).

  5. 2FA bypass — turn on WaboSign 2FA for the SSO user via Settings → Profile → Two-Factor Authentication. Sign out. Sign back in via Google. Confirm you go straight to the dashboard without an OTP prompt.

  6. Spec suitebin/rspec spec/requests/users/omniauth_callbacks_spec.rb runs five cases: happy path, link existing user, domain rejection, identity collision, 2FA bypass. All should pass.


Troubleshooting

Symptom Likely cause Fix
No Google button on sign-in page GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is unset, or the app hasn't been restarted since you set them. Check bin/rails runner 'puts Wabosign.google_sso_enabled?' — should print true. Restart if needed.
redirect_uri_mismatch error from Google The redirect URI registered in Google Cloud Console doesn't match the one WaboSign sends. Ensure the Authorized redirect URI in Google Cloud is exactly https://<your-host>/users/auth/google_oauth2/callback (matching scheme, host, no trailing slash). Update ENV['HOST'] / APP_URL on the WaboSign side if needed.
"Google sign-in failed: this Google account is not permitted" for a user whose domain is on the allowlist The Google account is a personal Gmail (no hd claim) rather than a Workspace account, or the hd claim differs from your allowlist entry (e.g. googlemail.com vs gmail.com). Confirm the user is signing in with their Workspace identity. Check the Rails logs around the failure for the actual hd value.
Identity collision rejection (existing email with different Google uid) Someone else's Google account already linked to that email, then the user changed their primary Google identity. Manually unset provider/uid on that user row via bin/rails console: User.find_by(email: '...').update_columns(provider: nil, uid: nil). The next sign-in will re-link.
User keeps being prompted for MFA setup after Google sign-in FORCE_MFA is enabled for the account, and the code path missed the bypass. Confirm User#signed_in_via_sso? returns true for that user (run in rails console). If it returns false, check that the user has provider: 'google_oauth2' and a non-blank uid — both are set on first SSO sign-in.
Sign-in works but the user lands in an empty/wrong account More than one Account exists in your deployment and the user got assigned to the wrong one by JIT provisioning. Set GOOGLE_DEFAULT_ACCOUNT_ID to pin the target account, or move the user via the Users settings page.
Boot log: "any Google account will be permitted to sign in" Allowlist is empty. Set GOOGLE_ALLOWED_DOMAINS to your Workspace domain(s) and restart.

Security notes

  • Domain allowlist enforced server-side. Google's hd parameter on the request is a UX hint — Google may still let unrelated accounts through the consent screen. WaboSign re-checks the hd claim in the OAuth response before issuing a session, so a misconfigured Google Cloud consent screen cannot bypass the allowlist.

  • Identity collision protection. If a row in users already has provider/uid set, sign-in via a different Google uid for the same email is rejected. This blocks "I changed my Workspace identity but kept the email alias" attacks.

  • Password sign-in is not weakened. Adding Google SSO does not remove or alter the password flow. Users who never click the Google button can keep using passwords + OTP exactly as before. An attacker who compromises your Google Cloud project can sign in as any allowlisted email — they cannot sign in as users on disallowed domains, nor as users who have no password yet (those don't exist after JIT — every JIT-created user has a random unused password, which is fine because it's not recoverable).

  • The unused password. JIT-created users have password = SecureRandom.hex(32). It is never displayed and never resetable through the standard flow (the user has no way to know it). To grant a JIT-only user a real password, use the Devise "forgot password" flow or set one via bin/rails console.

  • No domain-wide pre-revocation. Removing a user from your Google Workspace does not delete or disable their WaboSign user. Use the Users settings page to archive them, or write a periodic task that scans against Google's Admin SDK.


Code map

File Role
Gemfile Adds omniauth, omniauth-google-oauth2, omniauth-rails_csrf_protection.
lib/wabosign.rb GOOGLE_* constants, google_sso_enabled?, google_domain_allowed?, boot warning.
config/initializers/devise.rb Registers :google_oauth2 Devise OmniAuth strategy when enabled.
app/models/user.rb Conditional :omniauthable, from_google_omniauth, default_sso_account, signed_in_via_sso?.
config/routes.rb devise_for extended with omniauth_callbacks.
app/controllers/users/omniauth_callbacks_controller.rb Handles /users/auth/google_oauth2/callback.
app/controllers/sessions_controller.rb Clears session[:bypass_otp_for_sso] on sign-out.
app/controllers/dashboard_controller.rb Honours the SSO bypass flag to skip the FORCE_MFA redirect.
app/views/devise/sessions/_omniauthable.html.erb The Google button on the sign-in page.
app/views/sso_settings/_placeholder.html.erb Status panel at /settings/sso.
db/migrate/20260515200000_add_omniauth_to_users.rb Adds provider, uid, partial unique index.
public/google_g.svg Google "G" mark used by the button.
spec/requests/users/omniauth_callbacks_spec.rb Request specs (happy path, link, reject, collision, 2FA bypass).

Future work

  • Other IdPs. To support Okta / Entra / Keycloak, swap omniauth-google-oauth2 for omniauth_openid_connect (generic OIDC) and add per-IdP env vars. The User model JIT logic stays the same shape.
  • Admin UI for credentials. Move from env vars to an encrypted EncryptedConfig record so non-developers can rotate credentials. The existing SsoSettingsController already loads a saml_configs key — extend with google_oauth_configs and a form.
  • Role mapping. When you add :editor / :viewer roles, derive them from Google Workspace group claims rather than defaulting every JIT user to :admin.
  • Workspace-wide deprovisioning. Periodic Sidekiq job that uses the Google Admin SDK to check whether each Google-linked WaboSign user still exists in your Workspace, and archives ones that don't.