Skip to content

Add stub login for development - #16

Open
senid231 wants to merge 4 commits into
activeadmin-plugins:mainfrom
senid231:stub-login
Open

Add stub login for development#16
senid231 wants to merge 4 commits into
activeadmin-plugins:mainfrom
senid231:stub-login

Conversation

@senid231

@senid231 senid231 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Developing against an OIDC-only admin panel is painful: the IdP must know your local redirect URI, which breaks on a different port or when two apps share one client. Projects work around it with per-repo bypass controllers that skip the gem's provisioning path, so authorization is never exercised locally.

Adds one config method:

ActiveAdmin::Oidc.configure do |c|
  c.stub_dev_env_login! do |claims|
    claims.merge("groups" => [ADMIN_GROUP])
  end
end

The login page renders as always — same button, same label — but the button signs in with locally fabricated claims instead of redirecting to the IdP. A red warning sits above it while it is on. The block is optional and runs per sign-in, so it can return a different identity each time.

It is not a separate code path. The button POSTs to the gem's own callbacks controller, which hands the claims to the same UserProvisioner a real callback uses — identity lookup, the takeover guard, your on_login hook, oidc_raw_info and active_for_authentication? all still run. provider stays "oidc", so a stub row is an ordinary OIDC row that a later real SSO login still matches.

Safety is the method itself: stub_dev_env_login! is a no-op outside the development environment and the route is not drawn there. Nothing to flip off before a deploy, no boot guard to trip.

Also fixes the published login view templates, which used generator-escaped <%%= while being copied verbatim — a host that published the view got literal ERB tags rendered as page text.

OIDC-only admin panels are painful to develop against: the IdP has to
know your local redirect URI, which breaks the moment you run on a
different port or two apps share one client. Projects work around it
with per-repo bypass controllers that skip the gem's whole provisioning
path, so authorization is never exercised locally.

Adds `stub_login_enabled` / `stub_login_claims`, which put a second
button on the normal login page. Nothing is automatic: the page still
renders and the user still clicks. The button POSTs to the gem's own
callbacks controller, which runs the fabricated claims through the same
UserProvisioner a real callback uses -- identity lookup, takeover guard,
the host's on_login hook, oidc_raw_info and active_for_authentication?
all still apply.

The provider stays "oidc" rather than a separate stub value, so a stub
row is still matched by a later real SSO login instead of tripping the
takeover guard.

Guarded three ways: boot raises when enabled under the production env,
the route is not drawn when disabled, and the action re-checks both.
Enabling it logs a warning at boot and renders a banner naming the
identity the button signs in as.

Also fixes the published login view templates, which used generator
escaped `<%%=` while being copied verbatim -- a host that published the
view got literal ERB tags rendered as page text.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a guarded development-only stub login flow through the existing OIDC provisioning pipeline.

Changes:

  • Adds configurable stub claims, UI, routes, callbacks, and production safeguards.
  • Updates bundled and generated login templates, documentation, and tests.
  • Bumps the version to 2.2.0.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Summary
spec/unit/configuration_spec.rb Tests stub configuration and validation.
spec/requests/stub_login_spec.rb Tests stub-login behavior and safeguards.
spec/isolated/features/isolated_stub_login_spec.rb Tests isolated-engine routing.
spec/generators/install_generator_spec.rb Tests generated login templates.
README.md Documents stub-login configuration and usage.
lib/generators/active_admin/oidc/install/templates/sessions_new.html.erb Updates the generated legacy login view.
lib/generators/active_admin/oidc/install/templates/sessions_new_v4.html.erb Updates the generated ActiveAdmin 4 login view.
lib/generators/active_admin/oidc/install/templates/initializer.rb.tt Documents generated stub configuration.
lib/activeadmin/oidc/version.rb Bumps the version to 2.2.0.
lib/activeadmin/oidc/engine.rb Registers helpers, guards, and conditional routes.
lib/activeadmin/oidc/configuration.rb Adds stub settings and claim validation.
app/views/active_admin/devise/sessions/new.html.erb Adds stub-login UI to the bundled view.
app/helpers/active_admin/oidc/view_helpers.rb Provides login and route helpers.
app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb Handles stub provisioning and sign-in.
Suppressed comments (5)

app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb:65

  • This new stub endpoint calls the shared sign-in tail, whose after_sign_in_path_for falls back to the hardcoded /admin. In the documented isolated-engine setup, the added feature spec already has to avoid following this redirect because /admin is not routed there, so a user who clicks the stub button can be sent to a dead page instead of completing the login flow. Make the post-login destination route-aware for the mounted Devise/ActiveAdmin route set and assert the final location in the isolated integration spec.
          provision_and_sign_in(claims)

app/helpers/active_admin/oidc/view_helpers.rb:54

  • Once invalid claims are represented as ConfigurationError, this view helper still lets that exception escape while rendering the login page, because it only handles the normal missing-identity case with presence. A malformed stub_login_claims would therefore make GET /admin/login fail before the user can see the endpoint's configuration flash; rescue the configuration error here and return nil so the existing (unconfigured identity) banner is rendered.
      def activeadmin_oidc_stub_login_identity
        claims = activeadmin_oidc_config.resolved_stub_login_claims
        claims[activeadmin_oidc_config.identity_claim.to_s].presence

lib/activeadmin/oidc/configuration.rb:87

  • stub_login_claims is documented as a Hash (or a callable returning one), but a non-Hash value such as a String reaches (raw || {}).to_h and raises NoMethodError. The stub action only translates ConfigurationError, so a malformed configuration produces a 500 instead of the intended readable misconfiguration response. Validate the resolved value's type before calling to_h and raise ConfigurationError for invalid values.
        (raw || {}).to_h.transform_keys(&:to_s)

lib/activeadmin/oidc/configuration.rb:19

  • The default stub subject ("stub-uid") will normally differ from the real IdP subject for the same email. UserProvisioner first looks up (provider, uid) and then rejects an identity row that already has any provider/uid, so using the same "oidc" provider does not make the later real login match; it causes the takeover guard to reject it. Either require/configure a stable real subject for the stub, or add an explicit and security-reviewed transition for stub-created rows instead of relying on the provider value alone.
      DEFAULT_STUB_LOGIN_CLAIMS = {
        'sub'   => 'stub-uid',
        'email' => 'stub@example.com'

lib/activeadmin/oidc/engine.rb:159

  • enforce_stub_login_policy! only validates the claims here, so enabling stub login with on_login = nil (or another non-callable value) still boots even though Configuration#validate! explicitly requires that hook. The first stub POST then reaches @config.on_login.call and raises NoMethodError/500 instead of failing at boot as the configuration contract implies; run cfg.validate! in this guard before the stub-claims validation.
        cfg.validate_stub_login! unless cfg.stub_login_claims.respond_to?(:call)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@senid231
senid231 requested a review from Fivell August 25, 2026 09:37
Replace the three stub_login_* config accessors with one method:

    c.stub_dev_env_login! do |claims|
      claims.merge("groups" => [ADMIN_GROUP])
    end

The method is a no-op outside the development environment, so the boot
guard, the production checks and the validators it needed all go away.
The login page keeps its single button; config.login_submit_path points
it at the stub route while stub login is on.

Removed:

* app/helpers/active_admin/oidc/view_helpers.rb -- the views read the
  config directly, as they did before stub login existed. This also
  fixes a 500 on the generator-published view in hosts whose AdminUser
  lacks :omniauthable, where the helpers were never registered.
* Engine.enforce_stub_login_policy! and its after_initialize.
* Configuration#validate_stub_login! and #sso_configured?.
* The validate! patch, restoring main's behaviour.
* Isolated-engine support for the stub URL, now a documented limit.

Fixed:

* Claims are stringified all the way down, on a copy, so a nested
  symbol key no longer breaks stub/real parity and a mutating block
  cannot write back into the defaults.
* The claims block may return a Hash or mutate the one it is given.
* Stub sign-in gets its own flash instead of claiming OIDC succeeded.
* The route reads login_path at draw time, and draws from
  login_submit_path so the button and the route cannot drift apart.

Co-Authored-By: Clanker
`login_path` and `logout_path` were read inside the route append block.
On ActiveAdmin 4 routes are drawn lazily on the first request, so the
read happened long after the host initializer that set them -- and any
later `ActiveAdmin::Oidc.reset!` (the spec suite does this per example)
replaced the Configuration, so the block saw the defaults. The isolated
engine, which sets an engine-relative `login_path = "/login"`, drew
`/admin/login` instead and 404ed.

Capture both paths at after_initialize time, as before stub login. The
stub flag and `login_submit_path` stay a draw-time read, but through
`ActiveAdmin::Oidc.config` rather than a captured instance, because
`reset!` swaps the whole object and a redraw must see the current one.

Co-Authored-By: Clanker

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread README.md Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

2 participants