diff --git a/app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb b/app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb index 8de1062..160db64 100644 --- a/app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb +++ b/app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb @@ -74,14 +74,18 @@ def after_sign_in_path_for(resource) stored_location_for(resource) || '/admin' end - # Devise's default `after_omniauth_failure_path_for` calls - # `new_session_path(scope)`, a URL helper Devise only generates - # when :database_authenticatable mounts session routes. The - # engine mounts `new__session_path` itself (see - # `mount_oidc_sessions_routes` initializer) regardless of which - # modules are loaded, so we always route through that helper. + # Devise's `new_session_path(scope)` is only generated when + # `:database_authenticatable` is in the mapping's `used_helpers`, + # so an OIDC-only model never gets it. The engine mounts + # `new__session_path` itself, but the helper lives on + # whichever route set the mapping's `router_name` points at — + # main_app by default, or `` for hosts that mount + # Devise inside a Rails engine. Replicate Devise's own dispatch + # so the right context is asked. def after_omniauth_failure_path_for(scope) - public_send(:"new_#{scope}_session_path") + router_name = ::Devise.mappings[scope].router_name + context = router_name ? send(router_name) : self + context.public_send(:"new_#{scope}_session_path") end end end diff --git a/spec/engine/features/engine_mounted_login_spec.rb b/spec/engine/features/engine_mounted_login_spec.rb index 0302349..42a19e5 100644 --- a/spec/engine/features/engine_mounted_login_spec.rb +++ b/spec/engine/features/engine_mounted_login_spec.rb @@ -46,4 +46,23 @@ visit "/admin" expect(page).to have_current_path("/admin/login") end + + context "OmniAuth failure path" do + around do |ex| + saved = OmniAuth.config.mock_auth[:oidc] + OmniAuth.config.mock_auth[:oidc] = :invalid_credentials + ex.run + OmniAuth.config.mock_auth[:oidc] = saved + end + + scenario "failed OmniAuth callback lands back on the SSO landing page" do + # OmniauthCallbacksController#after_omniauth_failure_path_for has + # to resolve `new__session_path` against the engine's + # route set (Devise.router_name pins helpers there) — calling the + # helper directly on the controller would raise NoMethodError. + visit "/admin/login" + click_button ActiveAdmin::Oidc.config.login_button_label + expect(page).to have_current_path("/admin/login") + end + end end diff --git a/spec/isolated/features/isolated_engine_login_spec.rb b/spec/isolated/features/isolated_engine_login_spec.rb index 78a8120..5d8e00d 100644 --- a/spec/isolated/features/isolated_engine_login_spec.rb +++ b/spec/isolated/features/isolated_engine_login_spec.rb @@ -37,4 +37,23 @@ expect(page.status_code).to eq(200) expect(page.body).to include(ActiveAdmin::Oidc.config.login_button_label) end + + context "OmniAuth failure path" do + around do |ex| + saved = OmniAuth.config.mock_auth[:oidc] + OmniAuth.config.mock_auth[:oidc] = :invalid_credentials + ex.run + OmniAuth.config.mock_auth[:oidc] = saved + end + + scenario "failed OmniAuth callback lands back on the SSO landing page" do + # As in the non-isolated case, the failure handler must resolve + # the session helper through the engine — and via the isolated + # engine's mount prefix the effective path stays `/admin/login` + # because `config.login_path = '/login'` is engine-relative. + visit "/admin/login" + click_button ActiveAdmin::Oidc.config.login_button_label + expect(page).to have_current_path("/admin/login") + end + end end