Skip to content

Commit a2c78ae

Browse files
committed
Filter OIDC token params at any depth again
A nested id_token or refresh_token is always a secret, so only the generic callback names (code, state, session_state, nonce) need the top-level limit that keeps host attributes like data.attributes.state visible. Co-Authored-By: Clanker
1 parent ec7aca1 commit a2c78ae

4 files changed

Lines changed: 19 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
## Unreleased
44

5-
- Fix: the OIDC `filter_parameters` entry now matches only the exact top-level keys `code`, `code_verifier`, `state`, `session_state`, `nonce`, `id_token`, `access_token` and `refresh_token`, so host params such as `code_id`, `state_eq` or `order[state]` are no longer filtered from logs. If you relied on the old substring match to hide params like `invite_code` or `reset_code`, add them to your own `filter_parameters`.
5+
- Fix: the OIDC `filter_parameters` entries now match exact keys instead of substrings. `code_verifier`, `id_token`, `access_token` and `refresh_token` are filtered at any depth; `code`, `state`, `session_state` and `nonce` only at the top level. Host params such as `code_id`, `state_eq` or `order[state]` are no longer filtered from logs. If you relied on the old substring match to hide params like `invite_code` or `reset_code`, add them to your own `filter_parameters`.

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ The gem's Rails engine handles several things so host apps don't have to:
8383
* **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.
8484
* **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.
8585
* **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.
86-
* **Parameter filtering** — top-level `code`, `code_verifier`, `state`, `session_state`, `nonce`, `id_token`, `access_token` and `refresh_token` params are added to `Rails.application.config.filter_parameters`. Only those exact top-level keys are matched, so host params like `code_id`, `state_eq` or `order[state]` stay visible.
86+
* **Parameter filtering** — the OIDC keys are added to `Rails.application.config.filter_parameters` as exact-key matches. `code_verifier`, `id_token`, `access_token` and `refresh_token` are filtered at any depth. The generic `code`, `state`, `session_state` and `nonce` are filtered only as top-level params, where the callback sends them, so host params like `code_id`, `state_eq` or `order[state]` stay visible.
8787

8888
## Configuration
8989

@@ -410,7 +410,7 @@ The gem also adds a unique `(provider, uid)` partial index in its own install mi
410410

411411
### What's filtered from logs
412412

413-
The engine adds the top-level OIDC callback keys (`code`, `code_verifier`, `state`, `session_state`, `nonce`, `id_token`, `access_token`, `refresh_token`) to `Rails.application.config.filter_parameters`, so a mid-callback crash can't dump them into production logs. Only those exact top-level keys are matched: `code_id`, `state_eq` and nested params like `order[state]` are not filtered. Your own `filter_parameters` entries are preserved.
413+
The engine adds the OIDC keys to `Rails.application.config.filter_parameters`, so a mid-callback crash can't dump them into production logs. Keys are matched exactly. `code_verifier`, `id_token`, `access_token` and `refresh_token` are filtered at any depth; `code`, `state`, `session_state` and `nonce` only as top-level params. So `code_id`, `state_eq` and nested params like `order[state]` are not filtered. Your own `filter_parameters` entries are preserved.
414414

415415
## Logger
416416

‎lib/activeadmin/oidc/engine.rb‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,13 @@ def controllers
137137

138138
initializer 'activeadmin_oidc.filter_parameters' do |app|
139139
# ActiveSupport::ParameterFilter matches a regexp containing "\." against the dotted
140-
# full key (order.state), so this hides only top-level keys, where the IdP callback puts them.
141-
oidc_params = %w[code code_verifier state session_state nonce id_token access_token refresh_token]
142-
app.config.filter_parameters |= [/\A(?!.*\.)(?:#{Regexp.union(oidc_params).source})\z/i]
140+
# full key (data.attributes.state), so the generic callback names stay visible when nested.
141+
callback_params = %w[code state session_state nonce]
142+
token_params = %w[code_verifier id_token access_token refresh_token]
143+
app.config.filter_parameters |= [
144+
/\A(?!.*\.)(?:#{Regexp.union(callback_params).source})\z/i,
145+
/\A(?:#{Regexp.union(token_params).source})\z/i
146+
]
143147
end
144148

145149
# The gem is OIDC-first: mount our SSO landing page at /admin/login

‎spec/security_spec.rb‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,22 @@
1414
# dump the whole params hash (including `code`, `id_token`,
1515
# `access_token`, `refresh_token`) straight into production logs.
1616
let(:oidc_keys) { %w[code code_verifier state session_state nonce id_token access_token refresh_token] }
17-
let(:host_params) { { "code_id" => "1", "state_eq" => "2", "order" => { "state" => "shipped", "code" => "X" } } }
17+
let(:host_params) do
18+
{ "code_id" => "1", "state_eq" => "2", "data" => { "attributes" => { "state" => "shipped", "code" => "X" } } }
19+
end
20+
let(:nested_tokens) { %w[code_verifier id_token access_token refresh_token] }
1821

1922
[
2023
["raw", ->(filters) { filters }],
2124
["precompiled", ->(filters) { ActiveSupport::ParameterFilter.precompile_filters(filters) }]
2225
].each do |form, compile|
23-
it "filters the top-level OIDC keys and leaves host params visible (#{form} filters)" do
26+
it "filters OIDC keys and nested tokens, and leaves host params visible (#{form} filters)" do
2427
filter = ActiveSupport::ParameterFilter.new(compile.call(Rails.application.config.filter_parameters))
25-
result = filter.filter(oidc_keys.index_with("secret").merge(host_params))
28+
params = oidc_keys.index_with("secret").merge(host_params, "auth" => nested_tokens.index_with("secret"))
29+
result = filter.filter(params)
2630

27-
expect(result).to eq(oidc_keys.index_with("[FILTERED]").merge(host_params))
31+
expect(result).to eq(oidc_keys.index_with("[FILTERED]")
32+
.merge(host_params, "auth" => nested_tokens.index_with("[FILTERED]")))
2833
end
2934
end
3035
end

0 commit comments

Comments
 (0)