activeadmin-oidc 2.1.3 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 352c557fd647f3422d80408f88f874cb995d6ce588dd2265263b3d6bf0da0030
4
- data.tar.gz: 488b15eb1617beb855fba722dc841bee6cf3403e7b5782c5904bf92bc97d7f74
3
+ metadata.gz: 7c594b010c852e97fc78daab9679704e733ed49341d26cc6763942e3aa081e47
4
+ data.tar.gz: 66c0b9c1f7557dfb9edac28392547070641c2164df4d9963018e8c8c9d3e116d
5
5
  SHA512:
6
- metadata.gz: a687c6ff15b7338e64e4dd5719792ce345b1b582c3952df38b7d44dde287020a92e71f57de660d9c487016025e518fe4eb72a12fc1103ba7f7131b6fd2d34eab
7
- data.tar.gz: abcfbd2f2a94c224c12315dc096a0c7c2fea335be7620e44798c3a2f584872eac47543f6b647b2679052d0dd657e85d3f896c18205f2c00dcc9dc97b25d0bde6
6
+ metadata.gz: 15fde3a273751c975c14f9bbbc43b6984796049e86649a25d6b257075834e249fb48bbafa67f9b3da0e2c12c780c0b8ba9097ba469d596742ec44a18114febcf
7
+ data.tar.gz: a8a9449b8a4b8871af049b1e55071820045f307cec42fda3850db0e999894bd14996f3c1a5d60737326fe95cad726ef6cb04c21e1ac37d1c54e93713b04c8a2a
data/README.md CHANGED
@@ -112,6 +112,10 @@ ActiveAdmin::Oidc.configure do |c|
112
112
  # By default PKCE is enabled iff client_secret is blank. Override:
113
113
  # c.pkce = true
114
114
 
115
+ # --- Stub login (development only) ------------------------------------
116
+ # See "Stub login" below. A no-op outside the development environment.
117
+ # c.stub_dev_env_login!
118
+
115
119
  # --- Authorization hook (REQUIRED) ------------------------------------
116
120
  c.on_login = ->(admin_user, claims) {
117
121
  # ... see "The on_login hook" below
@@ -137,6 +141,8 @@ end
137
141
  | `access_denied_message` | generic | Flash shown on any denial |
138
142
  | `on_login` | — (required) | Authorization hook; see below |
139
143
 
144
+ `stub_dev_env_login!` is a method, not an option — see "Stub login" below.
145
+
140
146
  ## The `on_login` hook
141
147
 
142
148
  `on_login` is the **only** place authorization lives. The gem handles authentication (the user proved who they are via the IdP); deciding whether that user is allowed into the admin panel — and what they can see once they are in — is the host application's problem. The gem does not ship a role model.
@@ -243,8 +249,101 @@ AdminUser.last.oidc_raw_info
243
249
  * Clicking it POSTs to `/admin/auth/oidc` with a Rails CSRF token. The gem loads `omniauth-rails_csrf_protection` so OmniAuth 2.x delegates its authenticity check to Rails' forgery protection and `button_to` just works.
244
250
  * After a successful callback the user is signed in and redirected to `/admin` (not the host app's `/`, which may not exist).
245
251
  * **Disabled/locked users are rejected.** Devise's `active_for_authentication?` is checked after provisioning but before sign-in. If your model overrides this method (e.g. to check an `enabled` flag or Devise's `:lockable` module), the guard fires on OIDC sign-in too — the user sees an appropriate flash and is redirected to the login page.
252
+ * In development, `stub_dev_env_login!` repoints that same button at a local sign-in that never contacts the IdP — see "Stub login" below.
246
253
  * Logout goes through Devise's stock session destroy. No RP-initiated single-logout ping to the IdP — override the destroy action in your host app if you need that.
247
254
 
255
+ ## Stub login (development)
256
+
257
+ When OIDC is the only way in, local development gets harder than the feature
258
+ you were trying to build. Some providers accept a `localhost:3000` redirect
259
+ URI — until you need a different port, or a second app that also speaks OIDC on
260
+ the same machine. Registering and juggling those redirect URIs is work that has
261
+ nothing to do with the change you are making.
262
+
263
+ Stub login is the escape hatch. Turn it on and the login page renders exactly as
264
+ it always does — same button, same label — but the button signs in with locally
265
+ fabricated claims instead of redirecting to the IdP. A red warning sits above
266
+ it while it is on.
267
+
268
+ ```ruby
269
+ ActiveAdmin::Oidc.configure do |c|
270
+ c.on_login = ->(admin_user, claims) {
271
+ groups = Array(claims["groups"])
272
+ return false unless groups.include?(ADMIN_GROUP)
273
+
274
+ admin_user.super_admin = groups.include?("super-admins")
275
+ true
276
+ }
277
+
278
+ # Default claims: { "sub" => "stub-uid", "email" => "stub-dev@example.com" }
279
+ # The optional block patches or replaces them.
280
+ c.stub_dev_env_login! do |claims|
281
+ claims.merge("groups" => [ADMIN_GROUP])
282
+ end
283
+ end
284
+ ```
285
+
286
+ `stub_dev_env_login!` is a **no-op outside the development environment** and
287
+ returns `false` there. That is the whole safety story: nothing to flip off
288
+ before a deploy, no boot guard to trip, and no route drawn anywhere else.
289
+ Leave the call uncommented in the initializer if you want.
290
+
291
+ The block runs once per sign-in, not once at boot. To switch between distinct
292
+ users, vary both `sub` and the configured identity claim; changing only the
293
+ email retains `stub-uid` and updates the same `(provider, uid)` row.
294
+
295
+ ### It is not a separate code path
296
+
297
+ The button POSTs to the gem's own callbacks controller, which hands the claims
298
+ to the **same `UserProvisioner` a real callback uses**. That means identity
299
+ lookup by `(provider, uid)`, the account-takeover guard, your `on_login` hook,
300
+ `oidc_raw_info`, and Devise's `active_for_authentication?` check all still run.
301
+ Authorization you develop against the stub is the authorization you get against
302
+ the IdP.
303
+
304
+ Three consequences worth internalising:
305
+
306
+ * **Your `on_login` hook must accept the stub claims.** If it reads
307
+ `claims["groups"]` or Zitadel's nested roles claim, add them in the block —
308
+ otherwise the stub is denied, correctly. You will find out the first time you
309
+ click the button, and fix it once.
310
+ * **Use a made-up email, never your real one.** A stub sign-in writes the fake
311
+ `sub` onto that row. Your real sign-in later brings the real `sub`, they do not
312
+ match, and the takeover guard locks you out of that account until someone
313
+ clears the columns by hand.
314
+ * **`provider` is always `"oidc"`,** never a separate `"stub"`/`"test"` value,
315
+ so the row a stub sign-in creates is an ordinary OIDC row.
316
+
317
+ ### Limits
318
+
319
+ * **Development only.** Nothing else enables it.
320
+ * **Devise mounted inside an isolated engine is not supported.** The stub URL is
321
+ built as `"#{login_path}/stub"`, which does not account for a mount prefix.
322
+ The real SSO flow is unaffected.
323
+ * **CSRF protection is your app's.** The stub POST does not go through the
324
+ OmniAuth stack, so it relies on whatever your `ApplicationController` does. If
325
+ you have set `allow_forgery_protection = false` in `development.rb`, any web
326
+ page you visit can silently sign you into your local admin panel.
327
+
328
+ ### Hosts with a custom login view
329
+
330
+ If your app ships its own `app/views/active_admin/devise/sessions/new.html.erb`,
331
+ the engine backs off and your view wins, so you render the button yourself.
332
+ Everything you need is on the config object — no helpers to mix in:
333
+
334
+ ```erb
335
+ <% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
336
+ <p>Stub login is enabled: this button never contacts the identity provider.</p>
337
+ <% end %>
338
+
339
+ <%= button_to ActiveAdmin::Oidc.config.login_button_label,
340
+ ActiveAdmin::Oidc.config.login_submit_path,
341
+ method: :post, data: { turbo: false } %>
342
+ ```
343
+
344
+ `login_submit_path` returns the stub route while stub login is on and the real
345
+ OmniAuth entry point otherwise.
346
+
248
347
  ## Custom login view
249
348
 
250
349
  The gem ships a minimal SSO-only login page (a single button, no email/password fields). If you need a different layout — for instance, different branding, an explanatory paragraph, or multiple OmniAuth strategies — drop your own template at:
@@ -32,6 +32,50 @@ module ActiveAdmin
32
32
  claims['sub'] = auth['uid'] if claims['sub'].blank? && auth['uid'].present?
33
33
  claims['email'] = info['email'] if claims['email'].blank? && info['email'].present?
34
34
 
35
+ provision_and_sign_in(claims)
36
+ end
37
+
38
+ # Development stub login: sign in with locally fabricated claims
39
+ # instead of a real authorization code flow, for machines whose
40
+ # redirect URI the IdP does not know. Deliberately runs the SAME
41
+ # provisioning path as `#oidc` -- identity lookup, the takeover
42
+ # guard, the host's `on_login` hook, oidc_raw_info, and the
43
+ # active_for_authentication? check all still apply, so what works
44
+ # locally is what works against the real IdP. In particular, a
45
+ # claims block that does not satisfy `on_login` is denied here
46
+ # exactly as the real IdP would deny it.
47
+ #
48
+ # The route only exists when stub login is enabled, and
49
+ # `stub_dev_env_login!` only enables it in the development
50
+ # environment. The check here covers a flag flipped at runtime.
51
+ def stub
52
+ cfg = ActiveAdmin::Oidc.config
53
+ unless cfg.stub_dev_env_login_enabled?
54
+ head :not_found
55
+ return
56
+ end
57
+
58
+ claims = cfg.stub_dev_env_login_claims
59
+
60
+ ActiveAdmin::Oidc.logger.warn(
61
+ "[activeadmin-oidc] STUB LOGIN used for sub=#{claims['sub'].inspect} " \
62
+ '-- no identity provider was contacted.'
63
+ )
64
+
65
+ provision_and_sign_in(claims, kind: 'stub login (no IdP)')
66
+ end
67
+
68
+ def failure
69
+ Rails.logger.warn("[activeadmin-oidc] omniauth failure: #{failure_message}")
70
+ flash[:alert] = ActiveAdmin::Oidc.config.access_denied_message
71
+ redirect_to after_omniauth_failure_path_for(resource_name)
72
+ end
73
+
74
+ private
75
+
76
+ # Shared tail of every sign-in path: turn a claims hash into a
77
+ # persisted, signed-in admin user, or into a denial flash.
78
+ def provision_and_sign_in(claims, kind: 'OIDC')
35
79
  admin_user = UserProvisioner.new(
36
80
  ActiveAdmin::Oidc.config,
37
81
  claims: claims,
@@ -39,7 +83,7 @@ module ActiveAdmin
39
83
  ).call
40
84
 
41
85
  sign_in_and_redirect admin_user, event: :authentication
42
- set_flash_message(:notice, :success, kind: 'OIDC') if is_navigational_format?
86
+ set_flash_message(:notice, :success, kind: kind) if is_navigational_format?
43
87
  rescue ActiveAdmin::Oidc::InactiveError => e
44
88
  Rails.logger.warn("[activeadmin-oidc] inactive: #{e.inactive_message_key}")
45
89
  # Fall back to the standard `inactive` translation rather
@@ -57,14 +101,6 @@ module ActiveAdmin
57
101
  redirect_to after_omniauth_failure_path_for(resource_name)
58
102
  end
59
103
 
60
- def failure
61
- Rails.logger.warn("[activeadmin-oidc] omniauth failure: #{failure_message}")
62
- flash[:alert] = ActiveAdmin::Oidc.config.access_denied_message
63
- redirect_to after_omniauth_failure_path_for(resource_name)
64
- end
65
-
66
- private
67
-
68
104
  # Land on the ActiveAdmin namespace root after a successful SSO
69
105
  # sign-in instead of Devise's default (host app root). Hosts
70
106
  # that don't define a `/` route would otherwise hit a routing
@@ -1,11 +1,33 @@
1
1
  <% if ActiveAdmin::Oidc.aa_v4? %>
2
2
  <div class="p-6 sm:p-8 space-y-4 md:space-y-6 w-full sm:max-w-md bg-white sm:rounded-md shadow dark:border dark:bg-gray-800/50 dark:border-gray-800">
3
- <h2 class="text-xl font-bold text-gray-900 md:text-2xl dark:text-white flex gap-2 items-center">
4
- <%= site_title %> <%= set_page_title t('active_admin.devise.login.title') %>
5
- </h2>
3
+ <div class="flex gap-2 items-center">
4
+ <h2 class="text-xl font-bold text-gray-900 md:text-2xl dark:text-white flex gap-2 items-center">
5
+ <%= site_title %> <%= set_page_title t('active_admin.devise.login.title') %>
6
+ </h2>
7
+
8
+ <%# Active Admin's own dark mode toggle. The logged out layout renders
9
+ active_admin/html_head, which loads the JS that delegates clicks on
10
+ .dark-mode-toggle, so the button needs no script of its own. %>
11
+ <button type="button"
12
+ class="dark-mode-toggle ms-auto flex items-center w-9 h-9 justify-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-sm"
13
+ aria-label="<%= t('active_admin.toggle_dark_mode', default: 'Toggle dark mode') %>">
14
+ <svg class="hidden dark:block w-5 h-5 rtl:-scale-x-100" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.509 5.75c0-1.493.394-2.96 1.144-4.25h-.081a8.5 8.5 0 1 0 7.356 12.746A8.5 8.5 0 0 1 8.509 5.75Z"></path></svg>
15
+ <svg class="dark:hidden w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 3V1m0 18v-2M5.05 5.05 3.636 3.636m12.728 12.728L14.95 14.95M3 10H1m18 0h-2M5.05 14.95l-1.414 1.414M16.364 3.636 14.95 5.05M14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"></path></svg>
16
+ </button>
17
+ </div>
18
+
19
+ <% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
20
+ <%# Classes copied from Active Admin's own error flash. The gem's views are
21
+ outside the host's Tailwind content path, so only utilities Active Admin
22
+ itself already emits are compiled. %>
23
+ <div class="activeadmin-oidc-stub-login flex items-center gap-3 p-4 rounded-lg bg-red-50 text-red-800 dark:bg-red-800 dark:text-red-300">
24
+ <svg class="w-5 h-5 shrink-0 text-red-400 dark:text-red-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/></svg>
25
+ Stub login is enabled: the button below signs in without contacting the identity provider.
26
+ </div>
27
+ <% end %>
6
28
 
7
29
  <%= button_to ActiveAdmin::Oidc.config.login_button_label,
8
- "#{OmniAuth.config.path_prefix}/oidc",
30
+ ActiveAdmin::Oidc.config.login_submit_path,
9
31
  method: :post,
10
32
  class: "activeadmin-oidc-login-button w-full",
11
33
  form_class: 'formtastic',
@@ -15,7 +37,13 @@
15
37
  <div id="login">
16
38
  <h2><%= active_admin_application.site_title(self) %></h2>
17
39
 
18
- <%= form_tag "#{OmniAuth.config.path_prefix}/oidc",
40
+ <% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
41
+ <p class="activeadmin-oidc-stub-login" style="margin-top:1em;padding:0.75em;border:1px solid #c00;background:#fff3f3;color:#900;font-weight:bold">
42
+ Stub login is enabled: the button below signs in without contacting the identity provider.
43
+ </p>
44
+ <% end %>
45
+
46
+ <%= form_tag ActiveAdmin::Oidc.config.login_submit_path,
19
47
  method: :post,
20
48
  class: "activeadmin-oidc-login-form formtastic",
21
49
  data: { turbo: false } do %>
@@ -13,6 +13,10 @@ module ActiveAdmin
13
13
  'Your account has no permission to access this admin panel.'
14
14
  DEFAULT_LOGIN_PATH = '/admin/login'
15
15
  DEFAULT_LOGOUT_PATH = '/admin/logout'
16
+ DEFAULT_STUB_DEV_ENV_LOGIN_CLAIMS = {
17
+ 'sub' => 'stub-uid',
18
+ 'email' => 'stub-dev@example.com'
19
+ }.freeze
16
20
 
17
21
  attr_accessor :issuer, :client_id, :client_secret, :scope,
18
22
  :redirect_uri,
@@ -21,6 +25,12 @@ module ActiveAdmin
21
25
  :access_denied_message, :on_login, :admin_user_class,
22
26
  :login_path, :logout_path
23
27
 
28
+ # Readers, not writers: stub login is turned on through
29
+ # `stub_dev_env_login!` so the environment check cannot be skipped.
30
+ # Specs (this gem's own included) stub these two instead of
31
+ # pretending to run in the development environment.
32
+ attr_reader :stub_dev_env_login_claims_block
33
+
24
34
  def initialize
25
35
  reset!
26
36
  end
@@ -41,6 +51,8 @@ module ActiveAdmin
41
51
  @logout_path = DEFAULT_LOGOUT_PATH
42
52
  @on_login = nil
43
53
  @pkce_override = nil
54
+ @stub_dev_env_login = false
55
+ @stub_dev_env_login_claims_block = nil
44
56
  self
45
57
  end
46
58
 
@@ -54,6 +66,60 @@ module ActiveAdmin
54
66
  @pkce_override = value
55
67
  end
56
68
 
69
+ # Turns on the development stub login: the login page's button
70
+ # signs in with locally fabricated claims instead of redirecting to
71
+ # the IdP. For machines whose redirect URI the IdP does not know --
72
+ # a non-default port, or two apps sharing one OIDC client.
73
+ #
74
+ # A no-op outside the development environment, so there is nothing
75
+ # to guard at boot and nothing to flip off before a deploy.
76
+ #
77
+ # The optional block receives the default claims and returns the
78
+ # claims to sign in with, so a host whose `on_login` reads roles or
79
+ # groups can satisfy it:
80
+ #
81
+ # c.stub_dev_env_login! { |claims| claims.merge('groups' => ADMIN_GROUP) }
82
+ #
83
+ # The claims go through the same UserProvisioner as a real
84
+ # callback, so a block that does not satisfy `on_login` is denied
85
+ # exactly as the real IdP would deny it.
86
+ def stub_dev_env_login!(&block)
87
+ return false unless ::Rails.env.development?
88
+
89
+ @stub_dev_env_login = true
90
+ @stub_dev_env_login_claims_block = block
91
+ true
92
+ end
93
+
94
+ def stub_dev_env_login_enabled?
95
+ @stub_dev_env_login
96
+ end
97
+
98
+ # Evaluated once per stub sign-in, in the controller. String keys
99
+ # all the way down, the same shape `on_login` receives from a real
100
+ # callback.
101
+ #
102
+ # The block may either return a Hash or mutate the one it is given
103
+ # -- `claims["groups"] = [...]` as a last line returns the assigned
104
+ # value, not the Hash, and that should not 500 the dev's login.
105
+ def stub_dev_env_login_claims
106
+ claims = DEFAULT_STUB_DEV_ENV_LOGIN_CLAIMS.dup
107
+ block = stub_dev_env_login_claims_block
108
+ if block
109
+ returned = block.call(claims)
110
+ claims = returned if returned.is_a?(Hash)
111
+ end
112
+ claims.deep_transform_keys(&:to_s)
113
+ end
114
+
115
+ # Where the login page's single button POSTs to: the stub route
116
+ # while stub login is on, the real OmniAuth entry point otherwise.
117
+ def login_submit_path
118
+ return "#{login_path}/stub" if stub_dev_env_login_enabled?
119
+
120
+ "#{::OmniAuth.config.path_prefix}/#{Engine::PROVIDER_NAME}"
121
+ end
122
+
57
123
  def validate!
58
124
  raise ConfigurationError, 'issuer is required' if issuer.blank?
59
125
  raise ConfigurationError, 'client_id is required' if client_id.blank?
@@ -145,9 +145,12 @@ module ActiveAdmin
145
145
  app.config.after_initialize do
146
146
  next unless Engine.oidc_enabled?
147
147
 
148
- cfg = ActiveAdmin::Oidc.config
149
- login_path = cfg.login_path
150
- logout_path = cfg.logout_path
148
+ # Captured here, not inside the append block: on ActiveAdmin 4
149
+ # routes are drawn lazily on the first request, long after the
150
+ # host initializer that set these ran, so a draw-time read can
151
+ # observe a config that something else has since replaced.
152
+ login_path = ActiveAdmin::Oidc.config.login_path
153
+ logout_path = ActiveAdmin::Oidc.config.logout_path
151
154
  scope_name = Engine.admin_user_class.model_name.singular.to_sym
152
155
 
153
156
  Engine.session_routes_target(app).append do
@@ -174,6 +177,30 @@ module ActiveAdmin
174
177
  logout_via = [*::Devise.sign_out_via, aa_method].compact.uniq
175
178
 
176
179
  match logout_path, to: ::ActiveAdmin::Devise::SessionsController.action(:destroy), as: :"destroy_#{scope_name}_session", via: logout_via
180
+
181
+ # Development stub login. `stub_dev_env_login!` only ever
182
+ # sets the flag in the development environment, so the
183
+ # route simply does not exist anywhere else -- and a route
184
+ # that is never drawn cannot be probed.
185
+ # Read through `ActiveAdmin::Oidc.config` at draw time rather
186
+ # than from a captured object: `reset!` swaps the whole
187
+ # Configuration instance, and a redraw must see the current
188
+ # one.
189
+ stub_cfg = ActiveAdmin::Oidc.config
190
+
191
+ if stub_cfg.stub_dev_env_login_enabled?
192
+ # Resolve the controller per request instead of capturing
193
+ # `.action(:stub)` at draw time: the class lives in the
194
+ # engine's app/ and is reloadable, and stub login runs in
195
+ # development where a captured constant goes stale on the
196
+ # first reload. A lambda also sidesteps the module scoping
197
+ # an isolated engine would apply to a string target.
198
+ # `login_submit_path` is what the login button posts to,
199
+ # so drawing the route from it keeps the two in step.
200
+ post stub_cfg.login_submit_path,
201
+ to: ->(env) { ::ActiveAdmin::Oidc::Devise::OmniauthCallbacksController.action(:stub).call(env) },
202
+ as: :"#{scope_name}_stub_login"
203
+ end
177
204
  end
178
205
  end
179
206
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module Oidc
5
- VERSION = "2.1.3"
5
+ VERSION = "2.2.1"
6
6
  end
7
7
  end
@@ -21,6 +21,33 @@ ActiveAdmin::Oidc.configure do |c|
21
21
  # --- Login button label ----------------------------------------------
22
22
  # c.login_button_label = "Sign in with Corporate SSO"
23
23
 
24
+ # --- Stub login (development only) ------------------------------------
25
+ # Makes the login button sign in with locally fabricated claims instead
26
+ # of redirecting to the IdP. Nothing is automatic: the login page still
27
+ # renders and you still click. Useful when the IdP does not know your
28
+ # local redirect URI -- a non-default port, or two apps sharing one
29
+ # OIDC client.
30
+ #
31
+ # A no-op outside the development environment, so it is safe to leave
32
+ # uncommented. A red warning sits above the button while it is on.
33
+ #
34
+ # The claims run through the normal pipeline, `on_login` included, so
35
+ # authorization behaves exactly as it will against the real IdP.
36
+ # Default claims: { "sub" => "stub-uid", "email" => "stub-dev@example.com" }
37
+ #
38
+ # c.stub_dev_env_login!
39
+ #
40
+ # Pass a block to patch or replace the claims -- needed when your
41
+ # `on_login` reads roles, groups or anything else:
42
+ #
43
+ # c.stub_dev_env_login! do |claims|
44
+ # claims.merge("groups" => ["admins"])
45
+ # end
46
+ #
47
+ # Use a made-up email, not your real SSO one. A stub sign-in stamps the
48
+ # row with uid "stub-uid", and a later real sign-in for the same email
49
+ # is then refused by the takeover guard.
50
+
24
51
  # --- on_login hook ---------------------------------------------------
25
52
  # Called with (admin_user, claims) after identity lookup and before
26
53
  # save. Mutate admin_user in place, return truthy to allow sign-in,
@@ -1,11 +1,20 @@
1
+ <%# Development stub login. The submit path switches to the stub
2
+ route while ActiveAdmin::Oidc.config.stub_dev_env_login! is on,
3
+ which only ever happens in the development environment. %>
1
4
  <div id="login">
2
- <h2><%%= active_admin_application.site_title(self) %></h2>
5
+ <h2><%= active_admin_application.site_title(self) %></h2>
3
6
 
4
- <%%= form_tag "#{OmniAuth.config.path_prefix}/oidc",
7
+ <% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
8
+ <p class="activeadmin-oidc-stub-login" style="margin-top:1em;padding:0.75em;border:1px solid #c00;background:#fff3f3;color:#900;font-weight:bold">
9
+ Stub login is enabled: the button below signs in without contacting the identity provider.
10
+ </p>
11
+ <% end %>
12
+
13
+ <%= form_tag ActiveAdmin::Oidc.config.login_submit_path,
5
14
  method: :post,
6
15
  class: "activeadmin-oidc-login-form formtastic",
7
16
  data: { turbo: false } do %>
8
- <%%= submit_tag ActiveAdmin::Oidc.config.login_button_label,
17
+ <%= submit_tag ActiveAdmin::Oidc.config.login_button_label,
9
18
  class: "activeadmin-oidc-login-button" %>
10
- <%% end %>
19
+ <% end %>
11
20
  </div>
@@ -1,12 +1,37 @@
1
+ <%# Development stub login. The submit path switches to the stub
2
+ route while ActiveAdmin::Oidc.config.stub_dev_env_login! is on,
3
+ which only ever happens in the development environment. %>
1
4
  <div class="p-6 sm:p-8 space-y-4 md:space-y-6 w-full sm:max-w-md bg-white sm:rounded-md shadow dark:border dark:bg-gray-800/50 dark:border-gray-800">
2
- <h2 class="text-xl font-bold text-gray-900 md:text-2xl dark:text-white flex gap-2 items-center">
3
- <%%= site_title %> <%%= set_page_title t('active_admin.devise.login.title') %>
4
- </h2>
5
+ <div class="flex gap-2 items-center">
6
+ <h2 class="text-xl font-bold text-gray-900 md:text-2xl dark:text-white flex gap-2 items-center">
7
+ <%= site_title %> <%= set_page_title t('active_admin.devise.login.title') %>
8
+ </h2>
5
9
 
6
- <%%= button_to ActiveAdmin::Oidc.config.login_button_label,
7
- "#{OmniAuth.config.path_prefix}/oidc",
8
- method: :post,
9
- class: "activeadmin-oidc-login-button w-full",
10
- form_class: 'formtastic',
11
- data: { turbo: false } %>
10
+ <%# Active Admin's own dark mode toggle. The logged out layout renders
11
+ active_admin/html_head, which loads the JS that delegates clicks on
12
+ .dark-mode-toggle, so the button needs no script of its own. %>
13
+ <button type="button"
14
+ class="dark-mode-toggle ms-auto flex items-center w-9 h-9 justify-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-sm"
15
+ aria-label="<%= t('active_admin.toggle_dark_mode', default: 'Toggle dark mode') %>">
16
+ <svg class="hidden dark:block w-5 h-5 rtl:-scale-x-100" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.509 5.75c0-1.493.394-2.96 1.144-4.25h-.081a8.5 8.5 0 1 0 7.356 12.746A8.5 8.5 0 0 1 8.509 5.75Z"></path></svg>
17
+ <svg class="dark:hidden w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 3V1m0 18v-2M5.05 5.05 3.636 3.636m12.728 12.728L14.95 14.95M3 10H1m18 0h-2M5.05 14.95l-1.414 1.414M16.364 3.636 14.95 5.05M14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"></path></svg>
18
+ </button>
19
+ </div>
20
+
21
+ <% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
22
+ <%# Classes copied from Active Admin's own error flash. The gem's views are
23
+ outside the host's Tailwind content path, so only utilities Active Admin
24
+ itself already emits are compiled. %>
25
+ <div class="activeadmin-oidc-stub-login flex items-center gap-3 p-4 rounded-lg bg-red-50 text-red-800 dark:bg-red-800 dark:text-red-300">
26
+ <svg class="w-5 h-5 shrink-0 text-red-400 dark:text-red-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/></svg>
27
+ Stub login is enabled: the button below signs in without contacting the identity provider.
28
+ </div>
29
+ <% end %>
30
+
31
+ <%= button_to ActiveAdmin::Oidc.config.login_button_label,
32
+ ActiveAdmin::Oidc.config.login_submit_path,
33
+ method: :post,
34
+ class: "activeadmin-oidc-login-button w-full",
35
+ form_class: 'formtastic',
36
+ data: { turbo: false } %>
12
37
  </div>
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeadmin-oidc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.3
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Fedoronchuk
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-09-03 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activeadmin
@@ -254,6 +255,7 @@ description: |
254
255
  It builds on Devise + omniauth_openid_connect and adds JIT user provisioning,
255
256
  role mapping from provider claims via a host-owned on_login hook, and a
256
257
  single install generator that wires everything up.
258
+ email:
257
259
  executables: []
258
260
  extensions: []
259
261
  extra_rdoc_files: []
@@ -278,6 +280,7 @@ licenses:
278
280
  - MIT
279
281
  metadata:
280
282
  rubygems_mfa_required: 'true'
283
+ post_install_message:
281
284
  rdoc_options: []
282
285
  require_paths:
283
286
  - lib
@@ -292,7 +295,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
292
295
  - !ruby/object:Gem::Version
293
296
  version: '0'
294
297
  requirements: []
295
- rubygems_version: 3.7.1
298
+ rubygems_version: 3.5.22
299
+ signing_key:
296
300
  specification_version: 4
297
301
  summary: OpenID Connect SSO for ActiveAdmin
298
302
  test_files: []