From 45ed368a264bddf9f3f6634ce441c05b49f1bf1c Mon Sep 17 00:00:00 2001 From: Wabo Date: Fri, 15 May 2026 23:43:31 -0400 Subject: [PATCH] Fix Google SSO boot order and route plumbing surfaced by specs Three fixes uncovered while running the new omniauth_callbacks specs in a Ruby 4.0.1 container: - config/initializers/devise.rb: read GOOGLE_CLIENT_ID / SECRET / ALLOWED_DOMAINS directly from ENV instead of via Wabosign::*. The module isn't autoloadable yet at initializer-load time (Rails.root isn't set), but ENV is. The User model and controllers still go through Wabosign helpers, which load fine once Rails is up. - app/models/user.rb: stop passing `omniauth_providers:` when :omniauthable isn't in the modules list. Devise raises NoMethodError omniauth_providers= otherwise. Now both the module inclusion and the keyword are gated on Wabosign.google_sso_enabled? - spec/requests/users/omniauth_callbacks_spec.rb: post to user_google_oauth2_omniauth_callback_path instead of the hardcoded /users/auth/... URL. With devise_for :users, path: '/' the actual callback route is /auth/google_oauth2/callback. Also create a placeholder admin user so ApplicationController#maybe_redirect_to_setup doesn't intercept the request before the callback action runs. Schema dump and .gitignore (adds /vendor) bundled in. All 5 specs now pass. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + app/models/user.rb | 8 ++++++-- config/initializers/devise.rb | 15 +++++++++++---- db/schema.rb | 5 ++++- spec/requests/users/omniauth_callbacks_spec.rb | 13 ++++++++----- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index d14f4595..268cf028 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ yarn-debug.log* /ee dump.rdb *.onnx +/vendor diff --git a/app/models/user.rb b/app/models/user.rb index 5acd4eb7..deb156fe 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -70,8 +70,12 @@ class User < ApplicationRecord has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author devise_modules = %i[two_factor_authenticatable recoverable rememberable validatable trackable lockable] - devise_modules << :omniauthable if Wabosign.google_sso_enabled? - devise(*devise_modules, omniauth_providers: [:google_oauth2]) + devise_opts = {} + if Wabosign.google_sso_enabled? + devise_modules << :omniauthable + devise_opts[:omniauth_providers] = [:google_oauth2] + end + devise(*devise_modules, **devise_opts) attribute :role, :string, default: ADMIN_ROLE attribute :uuid, :string, default: -> { SecureRandom.uuid } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a240bf15..c51bee29 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -334,15 +334,22 @@ Devise.setup do |config| # changed. Defaults to true, so a user is signed in automatically after changing a password. # config.sign_in_after_change_password = true - if Wabosign.google_sso_enabled? + # NB: Wabosign-the-module relies on Rails.root, which isn't available yet + # when this initializer runs. Read ENV directly here so the omniauth strategy + # can be registered at boot. Controllers/models access the same values via + # Wabosign::GOOGLE_* once Rails is fully initialized. + google_client_id = ENV.fetch('GOOGLE_CLIENT_ID', nil) + google_client_secret = ENV.fetch('GOOGLE_CLIENT_SECRET', nil) + if google_client_id.present? && google_client_secret.present? config.omniauth :google_oauth2, - Wabosign::GOOGLE_CLIENT_ID, - Wabosign::GOOGLE_CLIENT_SECRET, + google_client_id, + google_client_secret, { scope: 'email,profile', prompt: 'select_account', access_type: 'online', - hd: Wabosign::GOOGLE_ALLOWED_DOMAINS.presence + hd: ENV.fetch('GOOGLE_ALLOWED_DOMAINS', '') + .split(',').map(&:strip).reject(&:empty?).presence } end diff --git a/db/schema.rb b/db/schema.rb index d8d7d1cb..8c95db68 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_06_121640) do +ActiveRecord::Schema[8.1].define(version: 2026_05_15_200000) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -513,17 +513,20 @@ ActiveRecord::Schema[8.1].define(version: 2026_05_06_121640) do t.datetime "locked_at" t.boolean "otp_required_for_login", default: false, null: false t.string "otp_secret" + t.string "provider" t.datetime "remember_created_at" t.datetime "reset_password_sent_at" t.string "reset_password_token" t.string "role", null: false t.integer "sign_in_count", default: 0, null: false + t.string "uid" t.string "unconfirmed_email" t.string "unlock_token" t.datetime "updated_at", null: false t.string "uuid", null: false t.index ["account_id"], name: "index_users_on_account_id" t.index ["email"], name: "index_users_on_email", unique: true + t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true, where: "(provider IS NOT NULL)" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true t.index ["uuid"], name: "index_users_on_uuid", unique: true diff --git a/spec/requests/users/omniauth_callbacks_spec.rb b/spec/requests/users/omniauth_callbacks_spec.rb index 18b0bd4d..73ec55fc 100644 --- a/spec/requests/users/omniauth_callbacks_spec.rb +++ b/spec/requests/users/omniauth_callbacks_spec.rb @@ -4,6 +4,9 @@ require 'rails_helper' RSpec.describe 'Google OAuth2 callback', type: :request do let!(:account) { create(:account) } + # ApplicationController redirects to /setup when no users exist; create a + # placeholder admin so that branch doesn't fire during these specs. + let!(:placeholder_admin) { create(:user, account: account, email: 'admin@wabo.cc') } before do OmniAuth.config.test_mode = true @@ -34,7 +37,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do stub_google_auth(email: 'new.user@wabo.cc') expect do - post '/users/auth/google_oauth2/callback' + post user_google_oauth2_omniauth_callback_path end.to change(User, :count).by(1) user = User.find_by(email: 'new.user@wabo.cc') @@ -52,7 +55,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do stub_google_auth(email: 'existing@wabo.cc', uid: 'google-uid-99') expect do - post '/users/auth/google_oauth2/callback' + post user_google_oauth2_omniauth_callback_path end.not_to change(User, :count) user.reload @@ -67,7 +70,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do stub_google_auth(email: 'outsider@evil.com', hd: 'evil.com') expect do - post '/users/auth/google_oauth2/callback' + post user_google_oauth2_omniauth_callback_path end.not_to change(User, :count) expect(response).to redirect_to(new_user_session_path) @@ -85,7 +88,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do it 'rejects sign-in when the email is linked to a different Google uid' do stub_google_auth(email: 'taken@wabo.cc', uid: 'different-uid') - post '/users/auth/google_oauth2/callback' + post user_google_oauth2_omniauth_callback_path user.reload expect(user.uid).to eq('original-uid') @@ -103,7 +106,7 @@ RSpec.describe 'Google OAuth2 callback', type: :request do it 'signs the user in via Google without prompting for OTP' do stub_google_auth(email: '2fa@wabo.cc', uid: '2fa-uid') - post '/users/auth/google_oauth2/callback' + post user_google_oauth2_omniauth_callback_path expect(response).to redirect_to(root_path) get root_path