Skip to content

Restore orphaned PR #10 merge: source SSO login path from OmniAuth.config.path_prefix#12

Merged
Fivell merged 4 commits into
mainfrom
high-risk-failing-tests
Jun 2, 2026
Merged

Restore orphaned PR #10 merge: source SSO login path from OmniAuth.config.path_prefix#12
Fivell merged 4 commits into
mainfrom
high-risk-failing-tests

Conversation

@Fivell
Copy link
Copy Markdown
Member

@Fivell Fivell commented Jun 2, 2026

Restores the orphaned merge of PR #10 (#10).

PR #10's base was set to high-risk-failing-tests so it could stack on top of #9. PR #9 merged to main at 14:40:29 (and its branch was deleted shortly after), but PR #10 was merged into the still-existing high-risk-failing-tests branch at 14:40:46. So PR #10's commit ended up on a branch that never made it back to main.

This PR brings that content (the view + the regression spec) straight onto main.

What's in the diff

app/views/active_admin/devise/sessions/new.html.erb  |  4 +-
spec/requests/login_path_helper_spec.rb              | 56 +++++++++++
2 files changed, 58 insertions(+), 2 deletions(-)

The login view now sources the form action from OmniAuth.config.path_prefix (Rack-level source of truth) instead of hardcoding /admin/auth/oidc. The hardcoded literal broke hosts that customise Devise.omniauth_path_prefix (different mount path, different sub-prefix for SSO) — their SSO button POSTed to a dead URL.

The companion spec stubs OmniAuth.config.path_prefix to a sentinel and asserts the rendered form action follows it.

Test plan

  • bundle exec rake spec:all — full suite green on this branch
  • CI matrix (Ruby 3.2/3.3/3.4 × ActiveAdmin 3.5/4.0) — was 6/6 green when the PR originally merged

Fivell added 4 commits June 2, 2026 16:28
Each spec is RED today and documents one production bug. The next
commit implements the fixes and turns them green.

1. on_login runs twice when two first sign-ins race. The loser hits
   RecordNotUnique, the provisioner retries, and the host's hook
   double-fires (audit log, webhook, welcome email all duplicated).

2. A disabled-by-hook user still gets a row in admin_users. The
   active_for_authentication? check runs in the controller AFTER the
   provisioner has already persisted. Hostile attempts grow the table.

3. A pre-seeded admin row (provider/uid still nil) gets adopted by
   anyone the IdP says owns that email - even when the IdP marks the
   email as unverified. Attacker registers ceo@example.com at a lax
   IdP, signs in, walks off with the CEO account.
Fix the three bugs covered by the failing specs in the previous commit.

1. Don't re-run on_login on the RecordNotUnique retry path. The flag
   set by save!'s rescue is now read in #call to short-circuit after
   the row is found: return the winner's record without re-invoking
   the host hook. Side effects fire exactly once.

2. Check active_for_authentication? inside the provisioner, between
   on_login and save!. If the hook flipped an inactivity flag, raise
   ProvisioningError BEFORE writing the row.

3. Refuse to adopt a legacy admin row when email_verified is
   explicitly false. IdPs that don't ship the claim keep the old
   behaviour - many never emit it, so a strict "must be true" check
   would break legitimate hosts.
The previous commit moved the active_for_authentication? guard into
the provisioner, but the controller's generic ProvisioningError
rescue replaced the model's I18n-translated reason ("Your account
is not activated yet.") with the catch-all access_denied_message.
Disabled users lost the specific feedback.

Add InactiveError (a ProvisioningError subclass that carries the
inactive_message symbol) and rescue it separately in the callbacks
controller, translating via I18n.t("devise.failure.<symbol>"). The
generic rescue stays in place for every other denial path.

Also drop the now-dead active_for_authentication? check in the
controller - the provisioner runs the same guard earlier and never
returns an inactive user.
The login view hardcoded /admin/auth/oidc. Hosts that change
Devise.omniauth_path_prefix (mounting Devise at a different scope,
say) ended up with a button POSTing to a dead URL.

Source the path from OmniAuth.config.path_prefix instead - it's the
single Rack-level value of where OmniAuth actually listens, and
Devise keeps it in sync with its own omniauth_path_prefix. Both view
branches (AA v3 form_tag and AA v4 button_to) updated.

Why not omniauth_authorize_path? Tried that first - it breaks in the
isolated engine case: Devise's helper resolves through engine routes
and the engine mount path gets prepended, producing
/admin/admin/auth/oidc. OmniAuth.config.path_prefix is plain Rack
and sidesteps the issue.

Regression spec stubs OmniAuth.config.path_prefix to a sentinel and
asserts the form action follows it. The spec's before block force-
loads routes first - on Rails 8 with lazy route loading, the stub
would otherwise still be active when devise_for runs, tripping
Devise's "path_prefix must match Devise.omniauth_path_prefix" guard.
@Fivell Fivell merged commit e1bfa0f into main Jun 2, 2026
6 checks passed
@Fivell Fivell deleted the high-risk-failing-tests branch June 2, 2026 15:05
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