From aa2c036bb01590a0f2a2b0a84bc4b7ef492ed583 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Tue, 2 Jun 2026 10:40:02 +0200 Subject: [PATCH 1/2] Fix after_omniauth_failure_path_for for engine-mounted Devise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devise's own new_session_path(scope) helper is only generated when :database_authenticatable is in the mapping's used_helpers — OIDC-only models never get it. The engine mounts new__session_path itself, but the helper lives on whichever route set the mapping's router_name points at: Rails.application.routes by default, or routes for hosts that mount Devise inside an engine. Replicates Devise's URL-helper dispatch in the OmniauthCallbacks failure handler: look up the mapping's router_name, send it on the controller to get the engine proxy (or fall back to self for main_app), then public_send the concrete session helper on that context. Without this fix the failure handler raised NoMethodError on hosts with Devise.router_name set (e.g. mount Devise inside a Console/AdminPanel engine). --- .../devise/omniauth_callbacks_controller.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 From 9fd30d872af04a816e44a3654a079817013eb0f1 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Tue, 2 Jun 2026 10:45:44 +0200 Subject: [PATCH 2/2] Cover OmniAuth failure path in engine + isolated suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both suites previously only tested the happy path (URL helpers resolve, GET /admin/login renders the SSO button). The failure handler in OmniauthCallbacksController#after_omniauth_failure_path_for was only exercised by spec/requests/omniauth_callback_spec.rb, which boots the default dummy where Devise.router_name is unset — so the engine-mounted regression that the previous commit fixes was uncovered by CI. Adds an :invalid_credentials Capybara scenario in each engine dummy: visit /admin/login, click the SSO button, expect to land back on /admin/login. Both fail without the controller's router_name-aware dispatch and pass with it. --- .../features/engine_mounted_login_spec.rb | 19 +++++++++++++++++++ .../features/isolated_engine_login_spec.rb | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) 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