activeadmin-oidc 2.2.0 → 3.0.0
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 +4 -4
- data/README.md +44 -6
- data/app/controllers/active_admin/oidc/devise/omniauth_callbacks_controller.rb +40 -15
- data/app/views/active_admin/devise/sessions/new.html.erb +21 -5
- data/lib/activeadmin/oidc/configuration.rb +92 -7
- data/lib/activeadmin/oidc/engine.rb +35 -17
- data/lib/activeadmin/oidc/version.rb +1 -1
- data/lib/activeadmin-oidc.rb +11 -0
- data/lib/generators/active_admin/oidc/install/templates/sessions_new_v4.html.erb +21 -5
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4482b8503e17fed870824c6a6d10bf09d5f5653a065edf1bdbf179f0cb5a8ebb
|
|
4
|
+
data.tar.gz: 6d8f00973fd12b22497e3d82a7b80f684e5ffc54ffa15f2a8b93493e825f76c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: daf83b73aaabff38e15b6c6e3ea4c3eb54c10f20e6736b543db6b0d603843ed605f8aa6e6bfe2ca31dab2e6c82a883abecd3a23a3fbd7cd233551206132db065
|
|
7
|
+
data.tar.gz: 625cd770049d4e7e4af7211a78fa1ffcd2cc0ddf603228935c0a620e2439cb9ba553c07d9cc6e38187e8a2d8b55ff4884f610e37864b366f4cc6259a2cec0cd4
|
data/README.md
CHANGED
|
@@ -50,16 +50,25 @@ OIDC is the only authentication mechanism — `:database_authenticatable`, `encr
|
|
|
50
50
|
|
|
51
51
|
If `devise_for :admin_users` lives inside a Rails engine (not the main app routes), set `Devise.router_name = :<engine_name>` in `config/initializers/devise.rb` and pass the same option to `devise_for`. The gem reads `Devise.available_router_name` and mounts its session routes inside that engine's route set, so `<Engine>.routes.url_helpers.new_<scope>_session_path` resolves correctly.
|
|
52
52
|
|
|
53
|
-
For **isolated** engines (`isolate_namespace ...`) mounted at a prefix (e.g. `mount AdminPanel::Engine => '/admin'`), the engine prepends its mount path to every internal route
|
|
53
|
+
For **isolated** engines (`isolate_namespace ...`) mounted at a prefix (e.g. `mount AdminPanel::Engine => '/admin'`), the engine prepends its mount path to every internal route, so the derived `login_path` of `/admin/login` would become `/admin/admin/login`. Configure engine-relative paths in `config/initializers/activeadmin_oidc.rb`:
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
56
|
ActiveAdmin::Oidc.configure do |c|
|
|
57
|
-
c.login_path
|
|
58
|
-
c.logout_path
|
|
57
|
+
c.login_path = '/login'
|
|
58
|
+
c.logout_path = '/logout'
|
|
59
|
+
c.omniauth_route_prefix = '/auth'
|
|
59
60
|
end
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
`omniauth_route_prefix` is the same idea applied to the routes Devise draws for
|
|
64
|
+
OmniAuth. It is separate from `omniauth_path_prefix` because the two are not the
|
|
65
|
+
same string here: the OmniAuth middleware sits in the *application's* Rack stack
|
|
66
|
+
and sees `/admin/auth/oidc` with the mount prefix still attached, while the route
|
|
67
|
+
Devise declares for the callback is inside the engine and gets `/admin` prepended
|
|
68
|
+
to it. Set only `omniauth_path_prefix` and the callback route lands on
|
|
69
|
+
`/admin/admin/auth/oidc/callback`, so the redirect the middleware issues 404s.
|
|
70
|
+
|
|
71
|
+
Non-isolated engines mounted at `/` don't need any of these overrides.
|
|
63
72
|
|
|
64
73
|
### 3. `config/initializers/activeadmin_oidc.rb` (generated)
|
|
65
74
|
|
|
@@ -73,7 +82,7 @@ The gem's Rails engine handles several things so host apps don't have to:
|
|
|
73
82
|
* **Callback controller** — the engine patches `ActiveAdmin::Devise.controllers` to route OmniAuth callbacks to the gem's controller. No manual `controllers: { omniauth_callbacks: ... }` needed in `routes.rb`.
|
|
74
83
|
* **Login view override** — the engine prepends an SSO-only login page (no email/password fields) to the sessions controller's view path. If your host app ships its own `app/views/active_admin/devise/sessions/new.html.erb`, the gem detects it and backs off — your view wins.
|
|
75
84
|
* **Session routes** — the engine mounts `GET /admin/login` (renders the SSO landing page) and `DELETE /admin/logout` under `devise_scope`, with the scope name derived from `config.admin_user_class`. Devise normally generates session routes as a side effect of `:database_authenticatable`; without that module the route helpers would not exist and ActiveAdmin's login redirect would 404.
|
|
76
|
-
* **Path prefix** — the engine
|
|
85
|
+
* **Path prefix** — the engine registers the strategy with `path_prefix: '/admin/auth'` so the middleware intercepts requests under ActiveAdmin's mount point, and sets `Devise.omniauth_path_prefix` to the prefix Devise declares its routes with. Compatible with Rails 7.2+ and Rails 8's lazy route loading.
|
|
77
86
|
* **Parameter filtering** — `code`, `id_token`, `access_token`, `refresh_token`, `state`, and `nonce` are added to `Rails.application.config.filter_parameters`.
|
|
78
87
|
|
|
79
88
|
## Configuration
|
|
@@ -137,12 +146,41 @@ end
|
|
|
137
146
|
| `identity_attribute` | `:email` | AdminUser column used for lookup/adoption |
|
|
138
147
|
| `identity_claim` | `:email` | Claim key read from the id_token/userinfo |
|
|
139
148
|
| `admin_user_class` | `"AdminUser"` | String or Class for the host's admin user model |
|
|
149
|
+
| `login_path` | `/<namespace>/login` | SSO landing page path; derived from ActiveAdmin's namespace |
|
|
150
|
+
| `logout_path` | `/<namespace>/logout` | Sign-out path; derived from ActiveAdmin's namespace |
|
|
151
|
+
| `omniauth_path_prefix` | `/<namespace>/auth` | Browser-visible path the OmniAuth middleware listens on; derived from ActiveAdmin's namespace |
|
|
152
|
+
| `omniauth_route_prefix` | `omniauth_path_prefix` | Prefix Devise declares its OmniAuth routes with; differs only for engine-mounted hosts |
|
|
140
153
|
| `login_button_label` | `"Sign in with SSO"` | Label on the login-page button |
|
|
141
154
|
| `access_denied_message` | generic | Flash shown on any denial |
|
|
142
155
|
| `on_login` | — (required) | Authorization hook; see below |
|
|
143
156
|
|
|
144
157
|
`stub_dev_env_login!` is a method, not an option — see "Stub login" below.
|
|
145
158
|
|
|
159
|
+
## ActiveAdmin's namespace
|
|
160
|
+
|
|
161
|
+
Everything the gem mounts hangs off ActiveAdmin's namespace, and all of it is
|
|
162
|
+
derived from `ActiveAdmin.application.default_namespace` rather than assumed to
|
|
163
|
+
be `admin`. A host that renames it:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# config/initializers/active_admin.rb
|
|
167
|
+
config.default_namespace = :backoffice
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
gets `/backoffice/login`, `/backoffice/logout`, the OmniAuth middleware at
|
|
171
|
+
`/backoffice/auth`, and a post-sign-in redirect to `/backoffice` — no gem
|
|
172
|
+
configuration needed. ActiveAdmin's root namespace (`config.default_namespace =
|
|
173
|
+
false`) mounts everything at the top level: `/login`, `/auth`, `/`.
|
|
174
|
+
|
|
175
|
+
Each is still overridable. Isolated engines *have* to override them, since the
|
|
176
|
+
engine's mount prefix is prepended to every path declared inside it — see
|
|
177
|
+
[Engine-mounted Devise](#engine-mounted-devise).
|
|
178
|
+
|
|
179
|
+
`omniauth_route_prefix` is what the gem assigns to `Devise.omniauth_path_prefix`,
|
|
180
|
+
and it is skipped entirely if your app already assigned that in
|
|
181
|
+
`config/initializers/devise.rb`. `omniauth_path_prefix` is passed to the OmniAuth
|
|
182
|
+
strategy directly, so it stays correct regardless.
|
|
183
|
+
|
|
146
184
|
## The `on_login` hook
|
|
147
185
|
|
|
148
186
|
`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.
|
|
@@ -247,7 +285,7 @@ AdminUser.last.oidc_raw_info
|
|
|
247
285
|
|
|
248
286
|
* A login button is added to the ActiveAdmin sessions page via a prepended view override — no templates to edit.
|
|
249
287
|
* 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.
|
|
250
|
-
* After a successful callback the user is signed in and redirected to
|
|
288
|
+
* After a successful callback the user is signed in and redirected to ActiveAdmin's namespace root (not the host app's `/`, which may not exist). The path comes from ActiveAdmin's own route helper, so a renamed `config.default_namespace` or an engine-mounted ActiveAdmin lands correctly; `/admin` is only the fallback when that helper cannot be resolved.
|
|
251
289
|
* **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
290
|
* In development, `stub_dev_env_login!` repoints that same button at a local sign-in that never contacts the IdP — see "Stub login" below.
|
|
253
291
|
* 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.
|
|
@@ -105,28 +105,53 @@ module ActiveAdmin
|
|
|
105
105
|
# sign-in instead of Devise's default (host app root). Hosts
|
|
106
106
|
# that don't define a `/` route would otherwise hit a routing
|
|
107
107
|
# error immediately after login, and even when `/` does exist
|
|
108
|
-
# it's rarely what an admin user wants to see.
|
|
109
|
-
# always mounts at `/admin`, so we go there directly.
|
|
108
|
+
# it's rarely what an admin user wants to see.
|
|
110
109
|
def after_sign_in_path_for(resource)
|
|
111
|
-
stored_location_for(resource) ||
|
|
110
|
+
stored_location_for(resource) || active_admin_root_path
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Resolved from ActiveAdmin's own route helper rather than
|
|
114
|
+
# assumed to be '/admin'. Two things move it: a host can rename
|
|
115
|
+
# the namespace (`config.default_namespace = :backoffice`), and
|
|
116
|
+
# an engine-mounted ActiveAdmin prefixes every path it declares
|
|
117
|
+
# with the engine's mount point -- so the real root can be
|
|
118
|
+
# '/admin/admin' or anything else entirely. Routing a signed-in
|
|
119
|
+
# admin to a 404 is a poor reward for a successful login.
|
|
120
|
+
def active_admin_root_path
|
|
121
|
+
send(devise_router_name(resource_name)).public_send(active_admin_root_helper)
|
|
122
|
+
rescue NameError, ::ActionController::UrlGenerationError
|
|
123
|
+
# The host has ActiveAdmin's routes somewhere we can't see (or
|
|
124
|
+
# hasn't drawn them at all). Its namespace prefix is the best
|
|
125
|
+
# remaining guess at where the admin panel lives -- except for
|
|
126
|
+
# the root namespace, where that prefix is '' and would make an
|
|
127
|
+
# empty, unredirectable Location.
|
|
128
|
+
ActiveAdmin::Oidc.config.active_admin_namespace_prefix.presence || '/'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ActiveAdmin names its namespace root helper `<namespace>_root_path`,
|
|
132
|
+
# except for the root namespace where it is plain `root_path`.
|
|
133
|
+
def active_admin_root_helper
|
|
134
|
+
namespace = ActiveAdmin::Oidc.config.active_admin_namespace
|
|
135
|
+
namespace ? :"#{namespace}_root_path" : :root_path
|
|
112
136
|
end
|
|
113
137
|
|
|
114
138
|
# Devise's `new_session_path(scope)` is only generated when
|
|
115
139
|
# `:database_authenticatable` is in the mapping's `used_helpers`,
|
|
116
140
|
# so an OIDC-only model never gets it. The engine mounts
|
|
117
|
-
# `new_<scope>_session_path` itself, but
|
|
118
|
-
#
|
|
119
|
-
# the per-mapping `router_name` (set by
|
|
120
|
-
# `devise_for :scope, router_name: :engine`) when present,
|
|
121
|
-
# otherwise the global `Devise.available_router_name`
|
|
122
|
-
# (set by `Devise.router_name = :engine`), which defaults to
|
|
123
|
-
# `:main_app`. Replicate that dispatcher here so the helper is
|
|
124
|
-
# resolved on the right context (Rails.application proxy or
|
|
125
|
-
# mounted engine proxy).
|
|
141
|
+
# `new_<scope>_session_path` itself, but on whichever route set
|
|
142
|
+
# `devise_router_name` resolves to.
|
|
126
143
|
def after_omniauth_failure_path_for(scope)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
send(devise_router_name(scope)).public_send(:"new_#{scope}_session_path")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Devise's URL helpers live on the per-mapping `router_name` (set
|
|
148
|
+
# by `devise_for :scope, router_name: :engine`) when present,
|
|
149
|
+
# otherwise on the global `Devise.available_router_name` (set by
|
|
150
|
+
# `Devise.router_name = :engine`), which defaults to `:main_app`.
|
|
151
|
+
# Replicate that dispatcher here so helpers resolve on the right
|
|
152
|
+
# context (Rails.application proxy or mounted engine proxy).
|
|
153
|
+
def devise_router_name(scope)
|
|
154
|
+
::Devise.mappings[scope]&.router_name || ::Devise.available_router_name
|
|
130
155
|
end
|
|
131
156
|
end
|
|
132
157
|
end
|
|
@@ -1,13 +1,29 @@
|
|
|
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
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
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>
|
|
6
18
|
|
|
7
19
|
<% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
|
|
8
|
-
|
|
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>
|
|
9
25
|
Stub login is enabled: the button below signs in without contacting the identity provider.
|
|
10
|
-
</
|
|
26
|
+
</div>
|
|
11
27
|
<% end %>
|
|
12
28
|
|
|
13
29
|
<%= button_to ActiveAdmin::Oidc.config.login_button_label,
|
|
@@ -11,19 +11,23 @@ module ActiveAdmin
|
|
|
11
11
|
DEFAULT_ADMIN_USER_CLASS = 'AdminUser'
|
|
12
12
|
DEFAULT_ACCESS_DENIED_MESSAGE =
|
|
13
13
|
'Your account has no permission to access this admin panel.'
|
|
14
|
-
DEFAULT_LOGIN_PATH = '/admin/login'
|
|
15
|
-
DEFAULT_LOGOUT_PATH = '/admin/logout'
|
|
16
14
|
DEFAULT_STUB_DEV_ENV_LOGIN_CLAIMS = {
|
|
17
15
|
'sub' => 'stub-uid',
|
|
18
16
|
'email' => 'stub-dev@example.com'
|
|
19
17
|
}.freeze
|
|
18
|
+
# Stands in for ActiveAdmin's namespace when ActiveAdmin is not
|
|
19
|
+
# loaded (plain unit specs, scripts) and it therefore cannot be
|
|
20
|
+
# read. Everything else derives from
|
|
21
|
+
# `ActiveAdmin.application.default_namespace`.
|
|
22
|
+
FALLBACK_NAMESPACE = :admin
|
|
20
23
|
|
|
21
24
|
attr_accessor :issuer, :client_id, :client_secret, :scope,
|
|
22
25
|
:redirect_uri,
|
|
23
26
|
:login_button_label, :timeout,
|
|
24
27
|
:identity_attribute, :identity_claim,
|
|
25
|
-
:access_denied_message, :on_login, :admin_user_class
|
|
26
|
-
|
|
28
|
+
:access_denied_message, :on_login, :admin_user_class
|
|
29
|
+
attr_writer :login_path, :logout_path,
|
|
30
|
+
:omniauth_path_prefix, :omniauth_route_prefix
|
|
27
31
|
|
|
28
32
|
# Readers, not writers: stub login is turned on through
|
|
29
33
|
# `stub_dev_env_login!` so the environment check cannot be skipped.
|
|
@@ -47,8 +51,10 @@ module ActiveAdmin
|
|
|
47
51
|
@identity_claim = DEFAULT_IDENTITY_CLAIM
|
|
48
52
|
@access_denied_message = DEFAULT_ACCESS_DENIED_MESSAGE
|
|
49
53
|
@admin_user_class = DEFAULT_ADMIN_USER_CLASS
|
|
50
|
-
@login_path =
|
|
51
|
-
@logout_path =
|
|
54
|
+
@login_path = nil
|
|
55
|
+
@logout_path = nil
|
|
56
|
+
@omniauth_path_prefix = nil
|
|
57
|
+
@omniauth_route_prefix = nil
|
|
52
58
|
@on_login = nil
|
|
53
59
|
@pkce_override = nil
|
|
54
60
|
@stub_dev_env_login = false
|
|
@@ -56,6 +62,82 @@ module ActiveAdmin
|
|
|
56
62
|
self
|
|
57
63
|
end
|
|
58
64
|
|
|
65
|
+
# The paths below all hang off ActiveAdmin's namespace, which the
|
|
66
|
+
# host can rename (`config.default_namespace = :backoffice`) -- in
|
|
67
|
+
# which case there is no /admin anywhere in the app and every
|
|
68
|
+
# hardcoded one would 404. They are computed on read rather than in
|
|
69
|
+
# `reset!` because the gem's own initializer may run before the
|
|
70
|
+
# host's `ActiveAdmin.setup` block.
|
|
71
|
+
#
|
|
72
|
+
# `login_path` and `logout_path` are declared inside whichever route
|
|
73
|
+
# set holds the host's Devise mapping, so an engine-mounted host has
|
|
74
|
+
# to override them engine-relative -- the mount prefix is prepended
|
|
75
|
+
# on top of whatever is written here.
|
|
76
|
+
def login_path
|
|
77
|
+
@login_path || "#{active_admin_namespace_prefix}/login"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def logout_path
|
|
81
|
+
@logout_path || "#{active_admin_namespace_prefix}/logout"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Where the OmniAuth middleware listens. This one is a real,
|
|
85
|
+
# browser-visible path: the middleware sits in the application's
|
|
86
|
+
# Rack stack and sees the URL before any engine mount prefix has
|
|
87
|
+
# been stripped.
|
|
88
|
+
def omniauth_path_prefix
|
|
89
|
+
@omniauth_path_prefix || "#{active_admin_namespace_prefix}/auth"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A host that set only `Devise.omniauth_path_prefix` meant one
|
|
93
|
+
# prefix, so it becomes ours too. An explicit value set here still
|
|
94
|
+
# wins: engine-mounted hosts need the two to differ by the mount
|
|
95
|
+
# prefix.
|
|
96
|
+
def adopt_devise_omniauth_path_prefix(prefix)
|
|
97
|
+
@omniauth_path_prefix ||= prefix.presence
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# What Devise declares its OmniAuth request/callback routes with.
|
|
101
|
+
# Devise reuses a single setting for both jobs, and the two differ
|
|
102
|
+
# by exactly the mount prefix when `devise_for` lives inside a
|
|
103
|
+
# mounted engine -- so an engine-mounted host sets this
|
|
104
|
+
# engine-relative ('/auth'), the same way it does `login_path`.
|
|
105
|
+
def omniauth_route_prefix
|
|
106
|
+
@omniauth_route_prefix || omniauth_path_prefix
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ActiveAdmin's namespace as a Symbol, or nil for the root
|
|
110
|
+
# namespace (`default_namespace = false`), which mounts everything
|
|
111
|
+
# at the top level.
|
|
112
|
+
def active_admin_namespace
|
|
113
|
+
namespace = active_admin_default_namespace
|
|
114
|
+
return nil if namespace.blank? || namespace.to_sym == :root
|
|
115
|
+
|
|
116
|
+
# `.underscore` to match ActiveAdmin: `Namespace#initialize` does
|
|
117
|
+
# `name.to_s.underscore`, so `default_namespace = :"admin-panel"`
|
|
118
|
+
# routes under /admin_panel. Deriving from the raw value would
|
|
119
|
+
# mount the login page at /admin-panel and 404 after sign-in.
|
|
120
|
+
namespace.to_s.underscore.to_sym
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Narrow on purpose: `NoMethodError` is what "ActiveAdmin is not
|
|
124
|
+
# loaded, or not set up yet" surfaces as. Anything else -- a host
|
|
125
|
+
# initializer blowing up inside its own `default_namespace`
|
|
126
|
+
# override, say -- is a real misconfiguration and must not be
|
|
127
|
+
# quietly turned into a wrong path.
|
|
128
|
+
def active_admin_default_namespace
|
|
129
|
+
return FALLBACK_NAMESPACE unless defined?(::ActiveAdmin) && ::ActiveAdmin.respond_to?(:application)
|
|
130
|
+
|
|
131
|
+
::ActiveAdmin.application.default_namespace
|
|
132
|
+
rescue NoMethodError
|
|
133
|
+
FALLBACK_NAMESPACE
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def active_admin_namespace_prefix
|
|
137
|
+
namespace = active_admin_namespace
|
|
138
|
+
namespace ? "/#{namespace}" : ''
|
|
139
|
+
end
|
|
140
|
+
|
|
59
141
|
def pkce
|
|
60
142
|
return @pkce_override unless @pkce_override.nil?
|
|
61
143
|
|
|
@@ -117,7 +199,10 @@ module ActiveAdmin
|
|
|
117
199
|
def login_submit_path
|
|
118
200
|
return "#{login_path}/stub" if stub_dev_env_login_enabled?
|
|
119
201
|
|
|
120
|
-
|
|
202
|
+
# `omniauth_path_prefix`, not `OmniAuth.config.path_prefix`:
|
|
203
|
+
# under an engine mount those two differ by the mount prefix,
|
|
204
|
+
# and this one is the browser-visible path the form posts to.
|
|
205
|
+
"#{omniauth_path_prefix}/#{Engine::PROVIDER_NAME}"
|
|
121
206
|
end
|
|
122
207
|
|
|
123
208
|
def validate!
|
|
@@ -7,6 +7,12 @@ module ActiveAdmin
|
|
|
7
7
|
class Engine < ::Rails::Engine
|
|
8
8
|
PROVIDER_NAME = :oidc
|
|
9
9
|
|
|
10
|
+
# True once the host has supplied the minimum OIDC credentials.
|
|
11
|
+
def self.configured?
|
|
12
|
+
cfg = ActiveAdmin::Oidc.config
|
|
13
|
+
cfg.issuer.present? && cfg.client_id.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
10
16
|
# True when the host's AdminUser model includes :omniauthable.
|
|
11
17
|
# Used to gate controller registration and view overrides so the
|
|
12
18
|
# gem is a no-op when OIDC is not enabled on the model.
|
|
@@ -77,21 +83,42 @@ module ActiveAdmin
|
|
|
77
83
|
# Automatically register the OmniAuth :openid_connect strategy with
|
|
78
84
|
# Devise when the gem is configured, so host apps don't have to
|
|
79
85
|
# duplicate the config.omniauth boilerplate in devise.rb.
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
86
|
+
#
|
|
87
|
+
# Ordered after the host's config/initializers (where
|
|
88
|
+
# `ActiveAdmin.setup` and `Devise.setup` live, so the namespace and
|
|
89
|
+
# any explicit host overrides are readable) and before
|
|
90
|
+
# `devise.omniauth`, which both needs the strategy registered and
|
|
91
|
+
# builds the middleware from these args.
|
|
92
|
+
#
|
|
93
|
+
# `path_prefix` is passed per-strategy rather than left to
|
|
94
|
+
# `OmniAuth.config.path_prefix`, because Devise overwrites that
|
|
95
|
+
# global at route-draw time with `Devise.omniauth_path_prefix` --
|
|
96
|
+
# the value it *declares its routes* with, which for a mounted
|
|
97
|
+
# engine is the same path minus the mount prefix. Setting the two
|
|
98
|
+
# independently through the global is impossible: Devise raises
|
|
99
|
+
# "Wrong OmniAuth configuration" whenever they disagree.
|
|
100
|
+
initializer 'activeadmin_oidc.register_omniauth_strategy',
|
|
101
|
+
after: :load_config_initializers, before: 'devise.omniauth' do
|
|
83
102
|
cfg = ActiveAdmin::Oidc.config
|
|
84
|
-
next
|
|
103
|
+
next unless Engine.configured?
|
|
85
104
|
|
|
86
105
|
require 'omniauth_openid_connect'
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
# `||=` semantics preserved: a host that set the prefix in its
|
|
108
|
+
# own devise.rb keeps it. Read by Devise when the routes are
|
|
109
|
+
# drawn, which happens later still.
|
|
110
|
+
host_prefix = ::Devise.omniauth_path_prefix
|
|
111
|
+
::Devise.omniauth_path_prefix ||= cfg.omniauth_route_prefix
|
|
112
|
+
|
|
113
|
+
# Devise's setting decides where the routes are DRAWN; the
|
|
114
|
+
# strategy's `path_prefix` decides where the middleware LISTENS,
|
|
115
|
+
# and the login view posts to it. If they differ, sign-in 404s.
|
|
116
|
+
cfg.adopt_devise_omniauth_path_prefix(host_prefix)
|
|
92
117
|
|
|
118
|
+
::Devise.setup do |devise|
|
|
93
119
|
devise.omniauth :openid_connect,
|
|
94
120
|
name: PROVIDER_NAME,
|
|
121
|
+
path_prefix: cfg.omniauth_path_prefix,
|
|
95
122
|
scope: (cfg.scope || 'openid email profile').split,
|
|
96
123
|
response_type: :code,
|
|
97
124
|
issuer: cfg.issuer,
|
|
@@ -106,15 +133,6 @@ module ActiveAdmin
|
|
|
106
133
|
host: nil
|
|
107
134
|
}.compact
|
|
108
135
|
end
|
|
109
|
-
|
|
110
|
-
# Devise propagates omniauth_path_prefix to
|
|
111
|
-
# OmniAuth.config.path_prefix during route generation
|
|
112
|
-
# (set_omniauth_path_prefix!). On Rails 8 routes load lazily,
|
|
113
|
-
# so the OmniAuth middleware may process requests before routes
|
|
114
|
-
# are drawn and miss the prefix. Set it eagerly here.
|
|
115
|
-
# Must happen AFTER `devise.omniauth` because that call
|
|
116
|
-
# triggers autoload of devise/omniauth which nils the value.
|
|
117
|
-
::OmniAuth.config.path_prefix = ::Devise.omniauth_path_prefix
|
|
118
136
|
end
|
|
119
137
|
|
|
120
138
|
initializer 'activeadmin_oidc.filter_parameters' do |app|
|
data/lib/activeadmin-oidc.rb
CHANGED
|
@@ -18,6 +18,17 @@ require "active_admin/version"
|
|
|
18
18
|
# at file load, so pull in the minimal Railtie base class first — this
|
|
19
19
|
# keeps the require safe even in spec_helper contexts where the full
|
|
20
20
|
# Rails stack hasn't been initialized yet.
|
|
21
|
+
#
|
|
22
|
+
# The delegation core ext has to come first. railties 8.1's
|
|
23
|
+
# `rails/initializable.rb` calls `delegate_missing_to` in the body of
|
|
24
|
+
# `Rails::Initializable::Collection` but only requires "tsort", and
|
|
25
|
+
# `rails/railtie.rb` requires `rails/initializable` before any
|
|
26
|
+
# ActiveSupport core ext. So on Rails 8.1 a bare `require
|
|
27
|
+
# "rails/railtie"` raises `NoMethodError: undefined method
|
|
28
|
+
# 'delegate_missing_to'`. Requiring the one core ext it needs keeps the
|
|
29
|
+
# intent above intact — we still load only the minimal Railtie base
|
|
30
|
+
# class, not all of Rails.
|
|
31
|
+
require "active_support/core_ext/module/delegation"
|
|
21
32
|
require "rails/railtie"
|
|
22
33
|
require "omniauth/rails_csrf_protection"
|
|
23
34
|
|
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
route while ActiveAdmin::Oidc.config.stub_dev_env_login! is on,
|
|
3
3
|
which only ever happens in the development environment. %>
|
|
4
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">
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
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>
|
|
9
|
+
|
|
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>
|
|
8
20
|
|
|
9
21
|
<% if ActiveAdmin::Oidc.config.stub_dev_env_login_enabled? %>
|
|
10
|
-
|
|
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>
|
|
11
27
|
Stub login is enabled: the button below signs in without contacting the identity provider.
|
|
12
|
-
</
|
|
28
|
+
</div>
|
|
13
29
|
<% end %>
|
|
14
30
|
|
|
15
31
|
<%= button_to ActiveAdmin::Oidc.config.login_button_label,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeadmin-oidc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Fedoronchuk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeadmin
|
|
@@ -288,7 +288,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
288
288
|
requirements:
|
|
289
289
|
- - ">="
|
|
290
290
|
- !ruby/object:Gem::Version
|
|
291
|
-
version: 3.
|
|
291
|
+
version: 3.3.0
|
|
292
292
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
293
293
|
requirements:
|
|
294
294
|
- - ">="
|