verikloak-rails 1.0.1 → 1.2.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/CHANGELOG.md +39 -0
- data/README.md +98 -3
- data/lib/generators/verikloak/install/templates/initializer.rb.erb +5 -0
- data/lib/verikloak/rails/configuration.rb +43 -2
- data/lib/verikloak/rails/controller/error_handling.rb +97 -0
- data/lib/verikloak/rails/controller.rb +34 -64
- data/lib/verikloak/rails/railtie.rb +30 -10
- data/lib/verikloak/rails/request_store_mirror.rb +72 -0
- data/lib/verikloak/rails/testing/claims_builder.rb +95 -0
- data/lib/verikloak/rails/testing/helpers.rb +59 -0
- data/lib/verikloak/rails/testing/middleware_stub.rb +95 -0
- data/lib/verikloak/rails/testing/rspec.rb +58 -0
- data/lib/verikloak/rails/testing.rb +28 -0
- data/lib/verikloak/rails/version.rb +1 -1
- data/lib/verikloak/rails.rb +1 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ab889b6413e152dc2be695f2f4f738d7e2b90712b54559cc596e6d1c92166ed
|
|
4
|
+
data.tar.gz: b97dd2c56aca7b021daf781ffc2ad834bc0b42c4b72efdd9cdda1d609495f4a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bdf562dbadfbede90f0d2d32ab69f62072636abd5a7ff0a3580a1fe6a58cb2dd24ee35a0468b4d0b1d5fe91174e9e49dc481b2cfee862a9030a909b58742c78
|
|
7
|
+
data.tar.gz: 3ec0f43f095dd1002e6f612af4061beda81f29da93b2bc6e09d0fb37d67dfce477d4cc4194705441e11318ad11902f37fac2ab42768b4986eb0fcdc092a3bd66
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.2.0] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`Testing::Helpers#build_pundit_user_context` crashed against the real `verikloak-pundit` gem**: the helper called `UserContext.new(user, claims)`, but verikloak-pundit's constructor is `UserContext.new(claims, resource_client: nil, config: nil)` (a single positional argument), so `build_admin_user_context` / `build_user_user_context` always raised `ArgumentError`. The helper now takes the claims Hash (`build_pundit_user_context(claims, **options)`) and forwards keyword options such as `resource_client:` to the real constructor. The unit-spec fake was aligned with the real signature, and new contract specs (see below) guard against future drift.
|
|
14
|
+
- **Controller helpers ignored custom `token_env_key` / `user_env_key`**: `current_user_claims` / `current_token` (and therefore `authenticate_user!`) always read the default `verikloak.user` / `verikloak.token` env keys, so configuring custom keys caused valid requests to be rejected with 401. The helpers and `Testing::MiddlewareStub` now resolve keys via the new `Configuration#effective_user_env_key` / `#effective_token_env_key`, matching what the middleware writes (and what `verikloak-pundit` already syncs to). Configured keys are whitespace-stripped exactly like the core middleware normalizes them before writing, so a padded value (e.g. an ENV var with a trailing newline) can no longer make the middleware write one env key while the helpers read another. `Testing::MiddlewareStub` resolves the keys when the stubbed call runs — not when the stub is installed — so configuration applied after `stub_verikloak_middleware` stays in sync with the helpers' request-time reads.
|
|
15
|
+
- **`require 'verikloak/rails'` failed outside a booted Rails app**: the Railtie now requires the ActiveSupport core extensions it depends on (`delegate_missing_to`, `blank?`) before `rails/railtie`, so the gem can be loaded standalone (e.g. from contract specs or plain scripts).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`Verikloak::Rails::RequestStoreMirror` middleware**: when the `request_store` gem is on the load path, the Railtie inserts this middleware right after `Verikloak::Middleware` to mirror `verikloak.user` / `verikloak.token` (or the configured custom keys) into `RequestStore.store`. Previously the controller helpers' RequestStore fallback documented in the README had no writer — nothing ever mirrored the values — so it only worked if the application mirrored them by hand. **Upgrade note**: the mirror overwrites `RequestStore.store[:verikloak_user]` / `[:verikloak_token]` on every request (`nil` on unauthenticated/skipped paths) so stale context never leaks between requests — remove any hand-rolled mirroring of these keys when upgrading. Mirroring failures never break the request, but are logged once (per middleware instance) instead of being silently swallowed.
|
|
19
|
+
- **`Configuration#effective_token_env_key` / `#effective_user_env_key`**: single source of truth for resolving the Rack env keys (custom value or core default), shared by the controller helpers, `RequestStoreMirror`, and `Testing::MiddlewareStub`.
|
|
20
|
+
- **Contract specs against the real sibling gems** (`spec/contracts`): verify the constructor/config surfaces of `verikloak` core, `verikloak-pundit`, `verikloak-bff`, and `verikloak-audience` that this gem relies on. Tagged `:contract` and excluded from the default run; the new CI `contracts` job runs them with `gemfiles/contracts.Gemfile` so interface drift is caught even though the unit suite uses fakes.
|
|
21
|
+
- **`jwks_refresh_interval` configuration option**: forwarded to the core `Verikloak::Middleware` (new in verikloak 1.1.0) to throttle JWKS/discovery revalidation on the request path. Left unset it is omitted from `middleware_options`, so the core default (`60` seconds) applies; `0` restores the pre-1.1 revalidate-on-every-request behavior, and numeric strings (e.g. from ENV) are coerced by the core. The keyword only exists in verikloak core `>= 1.1.0`, which this release now requires (see Changed).
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
- **Bumped locked dependencies to clear all Dependabot / bundler-audit advisories**: `jwt` 3.2.0 (GHSA-c32j-vqhx-rx3x, High), `faraday` 2.14.3 (GHSA-98m9-hrrm-r99r High, GHSA-5rv5-xj5j-3484 Medium), `net-imap` 0.6.4.1 (2 Medium, 1 Low), `nokogiri` 1.19.4 (9 advisories), `concurrent-ruby` 1.3.7 and `crass` 1.0.7 (3 + 4 advisories). All are transitive development/test dependencies pinned by `Gemfile.lock`; the gem's runtime requirements are unchanged.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **BREAKING**: `Testing::Helpers#build_pundit_user_context` now takes the claims Hash as its only positional argument (`build_pundit_user_context(claims, **options)` instead of `(user, claims)`). The old signature never worked against the real verikloak-pundit gem (it always raised `ArgumentError`), so only suites stubbing `UserContext` with a two-argument fake are affected; the `build_admin_user_context` / `build_user_user_context` wrappers are unchanged. See the Fixed entry above for details.
|
|
28
|
+
- **Raised the minimum `verikloak` (core) runtime dependency to `~> 1.1`** (was `~> 1.0`): verikloak-rails 1.2.0 forwards the `jwks_refresh_interval` keyword, which only exists in verikloak core `>= 1.1.0`. Enforcing it in the gemspec turns a would-be boot-time `ArgumentError` (setting the option against a 1.0.x core) into a clear dependency-resolution error at `bundle install`. Apps that do not use the option are still subject to the new floor, but core 1.1.0 is backward-compatible with 1.0.x apart from the new default JWKS refresh throttle (60s).
|
|
29
|
+
- **Error-handler registration hardened against boot-order issues**: `rescue_from StandardError` / `rescue_from Pundit::NotAuthorizedError` remain registered only when `render_500_json` / `rescue_pundit` are enabled — registering them unconditionally would suppress `ActiveSupport::Rescuable`'s `exception.cause` traversal (e.g. an app's `rescue_from ActiveRecord::RecordNotFound` no longer catching errors raised inside views and wrapped in `ActionView::Template::Error`) and shadow handlers registered earlier on the same class. Instead, the Railtie now fires the auto-include hook after `verikloak.configure`, so the include-time reads see the application's final configuration even when `ActionController::Base` is loaded early by another initializer. Handler bodies additionally re-check the flags at request time, so disabling them at runtime still takes effect. Note that Pundit must be loaded before the concern is included for the Pundit rescue to be registered.
|
|
30
|
+
- **Log-tag sanitization extracted** to a shared `_verikloak_sanitize_tag` helper (same `[[:cntrl:]]` stripping, applied to both `request_id` and `sub` tags).
|
|
31
|
+
- **Controller rescue handlers and 500-error logging extracted** to `Verikloak::Rails::Controller::ErrorHandling` (included by the concern automatically; behavior unchanged).
|
|
32
|
+
- `Gemfile.lock` now includes the `x86_64-linux` / `aarch64-linux` (glibc) platforms so `bundle install` works outside the Alpine (musl) Docker image.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [1.1.0] - 2026-05-09
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **RSpec testing helpers** (`Verikloak::Rails::Testing`): a reusable test-support layer so applications no longer have to hand-roll their own Verikloak stubs/claim builders. Composed of three independent modules — `ClaimsBuilder` (build OIDC-shaped claim Hashes from a user-like object), `MiddlewareStub` (stub `Verikloak::Middleware` and, when loaded, `Verikloak::BFF::HeaderGuard` / `Verikloak::Audience::Middleware` to inject claims into `env['verikloak.user']`), and `Helpers` (top-level mix-in plus `Verikloak::Pundit::UserContext` builders when `verikloak-pundit` is loaded). Require `verikloak/rails/testing/rspec` from your `rails_helper.rb` to mix the helpers into request and policy specs and to register the `with verikloak admin auth`, `with verikloak user auth`, and `with verikloak custom auth` shared contexts. Note: `MiddlewareStub` requires RSpec, and controller specs (`type: :controller`) bypass the Rack middleware stack and are therefore not auto-included; set `request.env['verikloak.user']` directly in those specs. See README "Testing Support" for usage.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **`ClaimsBuilder#build_jwt_claims`** falls back to the user's email when `username` / `preferred_username` is present but blank, preventing the `preferred_username` claim from being silently dropped by `.compact`.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
- **Bumped `rails` to `>= 8.1.3` and `json` to `>= 2.19.5`** in `Gemfile.lock` to clear known advisories surfaced by `bundler-audit` (Active Storage range-header DoS and json format-string injection).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
10
49
|
## [1.0.1] - 2026-03-08
|
|
11
50
|
|
|
12
51
|
### Fixed
|
data/README.md
CHANGED
|
@@ -53,7 +53,9 @@ Then configure `config/initializers/verikloak.rb`.
|
|
|
53
53
|
The helpers follow this priority order:
|
|
54
54
|
|
|
55
55
|
1. **Primary**: `request.env` (Rack environment) - Set directly by `Verikloak::Middleware`
|
|
56
|
-
2. **Fallback**: `RequestStore.store` (when available) - Thread-local storage for
|
|
56
|
+
2. **Fallback**: `RequestStore.store` (when available) - Thread-local storage for code running outside the controller
|
|
57
|
+
|
|
58
|
+
When the [`request_store`](https://rubygems.org/gems/request_store) gem is on the load path, the Railtie automatically inserts `Verikloak::Rails::RequestStoreMirror` right after `Verikloak::Middleware`. It mirrors the claims/token from the Rack env into `RequestStore.store` on every request, so the fallback works out of the box (e.g. in service objects or jobs enqueued during the request). The mirror overwrites `RequestStore.store[:verikloak_user]` / `[:verikloak_token]` on every request — with `nil` on unauthenticated or skipped paths — so stale context never leaks between requests; if you previously mirrored these keys by hand, remove your own writer when upgrading. Mirroring failures never break the request and are logged once so a dead fallback does not go unnoticed.
|
|
57
59
|
|
|
58
60
|
**Examples:**
|
|
59
61
|
|
|
@@ -62,8 +64,8 @@ The helpers follow this priority order:
|
|
|
62
64
|
current_user_claims # reads from request.env['verikloak.user']
|
|
63
65
|
current_token # reads from request.env['verikloak.token']
|
|
64
66
|
|
|
65
|
-
#
|
|
66
|
-
# (when
|
|
67
|
+
# Outside the controller during the same request
|
|
68
|
+
# (when the request_store gem is present; mirrored automatically)
|
|
67
69
|
current_user_claims # falls back to RequestStore.store[:verikloak_user]
|
|
68
70
|
current_token # falls back to RequestStore.store[:verikloak_token]
|
|
69
71
|
|
|
@@ -124,6 +126,7 @@ end
|
|
|
124
126
|
| --- | --- | --- |
|
|
125
127
|
| `Verikloak::Bff::HeaderGuard` (optional) | Before `Verikloak::Middleware` by default when the gem is present | Normalize or enforce trusted proxy headers such as `X-Forwarded-Access-Token` |
|
|
126
128
|
| `Verikloak::Middleware` | After `Rails::Rack::Logger` by default (configurable) | Validate Bearer JWT (OIDC discovery + JWKS), set `verikloak.user`/`verikloak.token`, and honor `skip_paths` |
|
|
129
|
+
| `Verikloak::Rails::RequestStoreMirror` (optional) | After `Verikloak::Middleware`, when the `request_store` gem is present | Mirror `verikloak.user`/`verikloak.token` into `RequestStore.store` for the controller helpers' fallback |
|
|
127
130
|
|
|
128
131
|
### BFF Integration
|
|
129
132
|
Support for BFF header handling (e.g., normalizing or enforcing `X-Forwarded-Access-Token`) now lives in a dedicated gem: verikloak-bff.
|
|
@@ -169,6 +172,7 @@ Keys under `config.verikloak`:
|
|
|
169
172
|
| `user_env_key` | String | Custom Rack env key that stores decoded claims | `nil` (middleware default `verikloak.user`) |
|
|
170
173
|
| `bff_header_guard_options` | Hash or Proc | Forwarded to `Verikloak::BFF.configure` prior to middleware insertion | `{}` |
|
|
171
174
|
| `allow_http` | Boolean | Allow `http://` discovery URLs (forwarded to core middleware). **Only for development/test.** | `false` |
|
|
175
|
+
| `jwks_refresh_interval` | Numeric or nil | Minimum seconds between JWKS revalidations on the request path; `0` revalidates on every request (pre-verikloak-1.1 behavior). Key rotation within the window still forces an immediate refresh. Numeric strings are coerced. | `nil` (verikloak default `60`) |
|
|
172
176
|
|
|
173
177
|
Environment variable examples are in the generated initializer.
|
|
174
178
|
|
|
@@ -220,10 +224,101 @@ end
|
|
|
220
224
|
| `audience` | `VERIKLOAK_AUDIENCE` |
|
|
221
225
|
| `issuer` | `VERIKLOAK_ISSUER` |
|
|
222
226
|
| `leeway` | `VERIKLOAK_LEEWAY` |
|
|
227
|
+
| `jwks_refresh_interval` | `VERIKLOAK_JWKS_REFRESH_INTERVAL` |
|
|
223
228
|
| `render_500_json` | `VERIKLOAK_RENDER_500` |
|
|
224
229
|
| `rescue_pundit` | `VERIKLOAK_RESCUE_PUNDIT` |
|
|
225
230
|
|
|
226
231
|
|
|
232
|
+
## Testing Support
|
|
233
|
+
|
|
234
|
+
`verikloak-rails` ships RSpec helpers so applications no longer need to
|
|
235
|
+
hand-roll their own Verikloak stubs and claim builders.
|
|
236
|
+
|
|
237
|
+
### Setup
|
|
238
|
+
|
|
239
|
+
Require the integration once from `spec/rails_helper.rb` (or
|
|
240
|
+
`spec/spec_helper.rb`):
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
require "verikloak/rails/testing/rspec"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
This mixes `Verikloak::Rails::Testing::Helpers` into request and policy
|
|
247
|
+
specs, and registers three shared contexts:
|
|
248
|
+
|
|
249
|
+
- `"with verikloak admin auth"` — authenticates as an admin (`groups: ["/admin"]`)
|
|
250
|
+
- `"with verikloak user auth"` — authenticates as a regular user (`groups: ["/user"]`)
|
|
251
|
+
- `"with verikloak custom auth"` — uses `let(:verikloak_groups)` /
|
|
252
|
+
`let(:verikloak_extra_claims)` to customise the injected claims
|
|
253
|
+
|
|
254
|
+
> Controller specs (`type: :controller`) bypass the Rack middleware
|
|
255
|
+
> stack, so `stub_verikloak_middleware` cannot inject claims through
|
|
256
|
+
> them. Prefer `type: :request`; if you must use a controller spec,
|
|
257
|
+
> set `request.env['verikloak.user']`/`request.env['verikloak.token']`
|
|
258
|
+
> directly in a `before` block.
|
|
259
|
+
|
|
260
|
+
The shared contexts assume a `current_user` factory exists (e.g.
|
|
261
|
+
`create(:user)`); override `let(:current_user)` to inject a different
|
|
262
|
+
object. The user object is duck-typed and only needs to respond to `uid`
|
|
263
|
+
and `email` (with optional `username` / `preferred_username` /
|
|
264
|
+
`first_name` / `last_name`).
|
|
265
|
+
|
|
266
|
+
### Request specs
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
RSpec.describe "Users API", type: :request do
|
|
270
|
+
include_context "with verikloak admin auth"
|
|
271
|
+
|
|
272
|
+
it "returns the users list" do
|
|
273
|
+
get "/api/v1/users"
|
|
274
|
+
expect(response).to have_http_status(:ok)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Or call the helpers directly when you need a custom claim shape:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
RSpec.describe "Custom claims", type: :request do
|
|
283
|
+
let(:user) { create(:user) }
|
|
284
|
+
|
|
285
|
+
before do
|
|
286
|
+
claims = build_jwt_claims(
|
|
287
|
+
user,
|
|
288
|
+
groups: ["/custom-group"],
|
|
289
|
+
extra_claims: { "custom" => "value" }
|
|
290
|
+
)
|
|
291
|
+
stub_verikloak_middleware(claims)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`stub_verikloak_middleware` automatically also stubs
|
|
297
|
+
`Verikloak::BFF::HeaderGuard` and `Verikloak::Audience::Middleware` when
|
|
298
|
+
those gems are loaded, and sets `env['verikloak.token']` so controller
|
|
299
|
+
helpers like `current_token` work. Custom `user_env_key` / `token_env_key`
|
|
300
|
+
settings are honored automatically.
|
|
301
|
+
|
|
302
|
+
### Policy specs (with `verikloak-pundit`)
|
|
303
|
+
|
|
304
|
+
When `verikloak-pundit` is loaded, `Verikloak::Pundit::UserContext`
|
|
305
|
+
builders become available:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
RSpec.describe UserPolicy, type: :policy do
|
|
309
|
+
let(:user) { create(:user) }
|
|
310
|
+
let(:admin_context) { build_admin_user_context(user) }
|
|
311
|
+
let(:user_context) { build_user_user_context(user) }
|
|
312
|
+
|
|
313
|
+
it "allows admin to index" do
|
|
314
|
+
expect(described_class.new(admin_context, User).index?).to be true
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
If `verikloak-pundit` is not loaded, calling the `*_user_context`
|
|
320
|
+
builders raises a clear error.
|
|
321
|
+
|
|
227
322
|
## Errors
|
|
228
323
|
This gem standardizes JSON error responses and HTTP statuses. See [ERRORS.md](ERRORS.md) for details and examples.
|
|
229
324
|
|
|
@@ -13,6 +13,11 @@ Rails.application.configure do
|
|
|
13
13
|
# JWT clock skew tolerance in seconds
|
|
14
14
|
config.verikloak.leeway = Integer(ENV.fetch('VERIKLOAK_LEEWAY', '60'))
|
|
15
15
|
|
|
16
|
+
# Minimum seconds between JWKS refresh checks on the request path.
|
|
17
|
+
# Leave unset to keep the verikloak default (60); 0 revalidates on every
|
|
18
|
+
# request.
|
|
19
|
+
# config.verikloak.jwks_refresh_interval = Integer(ENV.fetch('VERIKLOAK_JWKS_REFRESH_INTERVAL', '60'))
|
|
20
|
+
|
|
16
21
|
# Paths to skip authentication (health checks, etc.)
|
|
17
22
|
config.verikloak.skip_paths = %w[/up /health /rails/health]
|
|
18
23
|
|
|
@@ -56,6 +56,12 @@ module Verikloak
|
|
|
56
56
|
# Rack middleware to insert the header guard after.
|
|
57
57
|
# @return [Object, String, Symbol, nil]
|
|
58
58
|
class Configuration
|
|
59
|
+
# Default Rack env keys. These mirror the defaults used by the core
|
|
60
|
+
# `Verikloak::Middleware` and are the fallback when `token_env_key` /
|
|
61
|
+
# `user_env_key` are left unset.
|
|
62
|
+
DEFAULT_TOKEN_ENV_KEY = 'verikloak.token'
|
|
63
|
+
DEFAULT_USER_ENV_KEY = 'verikloak.user'
|
|
64
|
+
|
|
59
65
|
attr_accessor :discovery_url, :audience, :issuer, :leeway,
|
|
60
66
|
:logger_tags, :error_renderer, :auto_include_controller,
|
|
61
67
|
:render_500_json, :rescue_pundit,
|
|
@@ -64,7 +70,7 @@ module Verikloak
|
|
|
64
70
|
:bff_header_guard_insert_before, :bff_header_guard_insert_after,
|
|
65
71
|
:token_verify_options, :decoder_cache_limit,
|
|
66
72
|
:token_env_key, :user_env_key, :bff_header_guard_options,
|
|
67
|
-
:allow_http
|
|
73
|
+
:allow_http, :jwks_refresh_interval
|
|
68
74
|
|
|
69
75
|
attr_reader :skip_paths
|
|
70
76
|
|
|
@@ -92,6 +98,7 @@ module Verikloak
|
|
|
92
98
|
@user_env_key = nil
|
|
93
99
|
@bff_header_guard_options = {}
|
|
94
100
|
@allow_http = false
|
|
101
|
+
@jwks_refresh_interval = nil
|
|
95
102
|
@skip_path_matcher = nil
|
|
96
103
|
end
|
|
97
104
|
|
|
@@ -107,6 +114,26 @@ module Verikloak
|
|
|
107
114
|
@skip_path_matcher ||= SkipPathChecker.new(skip_paths)
|
|
108
115
|
end
|
|
109
116
|
|
|
117
|
+
# Rack env key actually used for the bearer token: the configured
|
|
118
|
+
# `token_env_key`, or the core middleware default when unset/blank.
|
|
119
|
+
# The value is whitespace-stripped to match the normalization the core
|
|
120
|
+
# middleware applies before writing to the env, so readers and writer
|
|
121
|
+
# always agree on the key. Shared by the controller helpers,
|
|
122
|
+
# RequestStore mirroring, and the testing middleware stub so all
|
|
123
|
+
# layers stay in sync.
|
|
124
|
+
# @return [String]
|
|
125
|
+
def effective_token_env_key
|
|
126
|
+
presence_or_default(token_env_key, DEFAULT_TOKEN_ENV_KEY)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Rack env key actually used for decoded claims: the configured
|
|
130
|
+
# `user_env_key`, or the core middleware default when unset/blank.
|
|
131
|
+
# Whitespace-stripped like {#effective_token_env_key}.
|
|
132
|
+
# @return [String]
|
|
133
|
+
def effective_user_env_key
|
|
134
|
+
presence_or_default(user_env_key, DEFAULT_USER_ENV_KEY)
|
|
135
|
+
end
|
|
136
|
+
|
|
110
137
|
# Options forwarded to the base Verikloak Rack middleware.
|
|
111
138
|
# @return [Hash]
|
|
112
139
|
# @example
|
|
@@ -123,9 +150,23 @@ module Verikloak
|
|
|
123
150
|
decoder_cache_limit: decoder_cache_limit,
|
|
124
151
|
token_env_key: token_env_key,
|
|
125
152
|
user_env_key: user_env_key,
|
|
126
|
-
allow_http: allow_http
|
|
153
|
+
allow_http: allow_http,
|
|
154
|
+
jwks_refresh_interval: jwks_refresh_interval
|
|
127
155
|
}.compact
|
|
128
156
|
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# @param value [String, nil]
|
|
161
|
+
# @param default [String]
|
|
162
|
+
# @return [String] the stripped value, or the default when blank.
|
|
163
|
+
# Stripping mirrors the core middleware's env-key normalization
|
|
164
|
+
# (`value.to_s.strip`); without it a padded key would make the
|
|
165
|
+
# middleware write one env key while the helpers read another.
|
|
166
|
+
def presence_or_default(value, default)
|
|
167
|
+
str = value.to_s.strip
|
|
168
|
+
str.empty? ? default : str
|
|
169
|
+
end
|
|
129
170
|
end
|
|
130
171
|
end
|
|
131
172
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Verikloak
|
|
6
|
+
module Rails
|
|
7
|
+
module Controller
|
|
8
|
+
# Rescue handlers and 500-error logging backing the Controller
|
|
9
|
+
# concern's `rescue_from` registrations. Turns uncaught exceptions
|
|
10
|
+
# into the standardized JSON responses while keeping the underlying
|
|
11
|
+
# failure visible to operators through the innermost logger.
|
|
12
|
+
module ErrorHandling
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# Handle uncaught StandardError: render the generic JSON 500 when
|
|
16
|
+
# `render_500_json` is enabled, otherwise re-raise so Rails' default
|
|
17
|
+
# error handling applies. The handler is only registered when the flag
|
|
18
|
+
# is enabled at include time; this request-time check additionally lets
|
|
19
|
+
# a runtime opt-out take effect.
|
|
20
|
+
#
|
|
21
|
+
# @param exception [StandardError]
|
|
22
|
+
# @return [void]
|
|
23
|
+
# @raise [StandardError] the original exception when rendering is disabled
|
|
24
|
+
def _verikloak_handle_standard_error(exception)
|
|
25
|
+
raise exception unless Verikloak::Rails.config.render_500_json
|
|
26
|
+
|
|
27
|
+
_verikloak_render_internal_error(exception)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Handle `Pundit::NotAuthorizedError`: render 403 JSON when
|
|
31
|
+
# `rescue_pundit` is enabled; otherwise fall through to the generic
|
|
32
|
+
# StandardError handling (500 JSON or re-raise).
|
|
33
|
+
#
|
|
34
|
+
# @param exception [StandardError]
|
|
35
|
+
# @return [void]
|
|
36
|
+
# @raise [StandardError] the original exception when both rescues are disabled
|
|
37
|
+
def _verikloak_handle_pundit_error(exception)
|
|
38
|
+
if Verikloak::Rails.config.rescue_pundit
|
|
39
|
+
render json: { error: 'forbidden', message: exception.message }, status: :forbidden
|
|
40
|
+
else
|
|
41
|
+
_verikloak_handle_standard_error(exception)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Log the exception and render the static JSON 500 body.
|
|
46
|
+
#
|
|
47
|
+
# @param exception [Exception]
|
|
48
|
+
# @return [void]
|
|
49
|
+
def _verikloak_render_internal_error(exception)
|
|
50
|
+
_verikloak_log_internal_error(exception)
|
|
51
|
+
render json: { error: 'internal_server_error', message: 'An unexpected error occurred' },
|
|
52
|
+
status: :internal_server_error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Write StandardError details to the controller or Rails logger when
|
|
56
|
+
# rendering the generic 500 JSON response. Logging ensures the
|
|
57
|
+
# underlying failure is still visible to operators even though the
|
|
58
|
+
# response body is static.
|
|
59
|
+
#
|
|
60
|
+
# @param exception [Exception]
|
|
61
|
+
# @return [void]
|
|
62
|
+
def _verikloak_log_internal_error(exception)
|
|
63
|
+
target_logger = _verikloak_base_logger
|
|
64
|
+
return unless target_logger.respond_to?(:error)
|
|
65
|
+
|
|
66
|
+
target_logger.error("[Verikloak] #{exception.class}: #{exception.message}")
|
|
67
|
+
backtrace = exception.backtrace
|
|
68
|
+
target_logger.error(backtrace.join("\n")) if backtrace&.any?
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Never allow logging failures to interfere with request handling.
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Locate the innermost logger that responds to `error`.
|
|
75
|
+
# @return [Object, nil]
|
|
76
|
+
def _verikloak_base_logger
|
|
77
|
+
root_logger = if defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
78
|
+
::Rails.logger
|
|
79
|
+
elsif respond_to?(:logger)
|
|
80
|
+
logger
|
|
81
|
+
end
|
|
82
|
+
current = root_logger
|
|
83
|
+
seen = Set.new
|
|
84
|
+
while current.respond_to?(:logger)
|
|
85
|
+
break unless seen.add?(current.object_id)
|
|
86
|
+
|
|
87
|
+
next_logger = current.logger
|
|
88
|
+
break if next_logger.nil? || next_logger.equal?(current)
|
|
89
|
+
|
|
90
|
+
current = next_logger
|
|
91
|
+
end
|
|
92
|
+
current
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_support/concern'
|
|
4
|
-
|
|
4
|
+
require_relative 'controller/error_handling'
|
|
5
5
|
|
|
6
6
|
module Verikloak
|
|
7
7
|
module Rails
|
|
@@ -13,20 +13,24 @@ module Verikloak
|
|
|
13
13
|
module Controller
|
|
14
14
|
extend ActiveSupport::Concern
|
|
15
15
|
|
|
16
|
+
# Rescue handlers and 500-error logging (kept in a separate module so
|
|
17
|
+
# the concern itself stays focused on wiring and public helpers).
|
|
18
|
+
include ErrorHandling
|
|
19
|
+
|
|
16
20
|
included do
|
|
17
21
|
before_action :authenticate_user!
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
# Handlers are only registered when enabled at include time: a
|
|
23
|
+
# registered-but-disabled `rescue_from StandardError` would suppress
|
|
24
|
+
# ActiveSupport::Rescuable's `exception.cause` traversal and shadow
|
|
25
|
+
# handlers registered earlier on the same class (re-raising from
|
|
26
|
+
# inside a handler consults no other handler). The Railtie fires the
|
|
27
|
+
# auto-include hook after `verikloak.configure`, so these flags are
|
|
28
|
+
# final here; the same applies to `defined?(::Pundit::NotAuthorizedError)`,
|
|
29
|
+
# which requires Pundit to be loaded before this concern is included.
|
|
30
|
+
# Generic handler first so specific handlers take precedence.
|
|
31
|
+
rescue_from StandardError, with: :_verikloak_handle_standard_error if Verikloak::Rails.config.render_500_json
|
|
26
32
|
if defined?(::Pundit::NotAuthorizedError) && Verikloak::Rails.config.rescue_pundit
|
|
27
|
-
rescue_from ::Pundit::NotAuthorizedError
|
|
28
|
-
render json: { error: 'forbidden', message: e.message }, status: :forbidden
|
|
29
|
-
end
|
|
33
|
+
rescue_from ::Pundit::NotAuthorizedError, with: :_verikloak_handle_pundit_error
|
|
30
34
|
end
|
|
31
35
|
rescue_from ::Verikloak::Error do |e|
|
|
32
36
|
Verikloak::Rails.config.error_renderer.render(self, e)
|
|
@@ -54,17 +58,19 @@ module Verikloak
|
|
|
54
58
|
def authenticated? = current_user_claims.present?
|
|
55
59
|
|
|
56
60
|
# The verified JWT claims for the current user.
|
|
57
|
-
# Prefer Rack env; fall back to
|
|
61
|
+
# Prefer Rack env (honoring a custom `user_env_key`); fall back to
|
|
62
|
+
# RequestStore when available.
|
|
58
63
|
# @return [Hash, nil]
|
|
59
64
|
def current_user_claims
|
|
60
|
-
_verikloak_fetch_request_context(
|
|
65
|
+
_verikloak_fetch_request_context(Verikloak::Rails.config.effective_user_env_key, :verikloak_user)
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
# The raw bearer token used for the current request.
|
|
64
|
-
# Prefer Rack env; fall back to
|
|
69
|
+
# Prefer Rack env (honoring a custom `token_env_key`); fall back to
|
|
70
|
+
# RequestStore when available.
|
|
65
71
|
# @return [String, nil]
|
|
66
72
|
def current_token
|
|
67
|
-
_verikloak_fetch_request_context(
|
|
73
|
+
_verikloak_fetch_request_context(Verikloak::Rails.config.effective_token_env_key, :verikloak_token)
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
# The `sub` (subject) claim from the current user claims.
|
|
@@ -105,20 +111,24 @@ module Verikloak
|
|
|
105
111
|
config = Verikloak::Rails.config
|
|
106
112
|
tags = []
|
|
107
113
|
if config.logger_tags.include?(:request_id)
|
|
108
|
-
rid = request.request_id || request.headers['X-Request-Id']
|
|
109
|
-
|
|
110
|
-
tags << "req:#{rid}" unless rid.empty?
|
|
114
|
+
rid = _verikloak_sanitize_tag(request.request_id || request.headers['X-Request-Id'])
|
|
115
|
+
tags << "req:#{rid}" if rid
|
|
111
116
|
end
|
|
112
117
|
if config.logger_tags.include?(:sub)
|
|
113
|
-
sub = current_subject
|
|
114
|
-
if sub
|
|
115
|
-
sanitized = sub.to_s.gsub(/[[:cntrl:]]+/, ' ').strip
|
|
116
|
-
tags << "sub:#{sanitized}" unless sanitized.empty?
|
|
117
|
-
end
|
|
118
|
+
sub = _verikloak_sanitize_tag(current_subject)
|
|
119
|
+
tags << "sub:#{sub}" if sub
|
|
118
120
|
end
|
|
119
121
|
tags
|
|
120
122
|
end
|
|
121
123
|
|
|
124
|
+
# Strip control characters and surrounding whitespace from a log tag value.
|
|
125
|
+
# @param value [Object, nil]
|
|
126
|
+
# @return [String, nil] sanitized value, or nil when blank
|
|
127
|
+
def _verikloak_sanitize_tag(value)
|
|
128
|
+
sanitized = value.to_s.gsub(/[[:cntrl:]]+/, ' ').strip
|
|
129
|
+
sanitized.empty? ? nil : sanitized
|
|
130
|
+
end
|
|
131
|
+
|
|
122
132
|
# Retrieve request context from Rack env or RequestStore.
|
|
123
133
|
# @param env_key [String]
|
|
124
134
|
# @param store_key [Symbol]
|
|
@@ -133,46 +143,6 @@ module Verikloak
|
|
|
133
143
|
|
|
134
144
|
store[store_key]
|
|
135
145
|
end
|
|
136
|
-
|
|
137
|
-
# Write StandardError details to the controller or Rails logger when
|
|
138
|
-
# rendering the generic 500 JSON response. Logging ensures the
|
|
139
|
-
# underlying failure is still visible to operators even though the
|
|
140
|
-
# response body is static.
|
|
141
|
-
#
|
|
142
|
-
# @param exception [Exception]
|
|
143
|
-
# @return [void]
|
|
144
|
-
def _verikloak_log_internal_error(exception)
|
|
145
|
-
target_logger = _verikloak_base_logger
|
|
146
|
-
return unless target_logger.respond_to?(:error)
|
|
147
|
-
|
|
148
|
-
target_logger.error("[Verikloak] #{exception.class}: #{exception.message}")
|
|
149
|
-
backtrace = exception.backtrace
|
|
150
|
-
target_logger.error(backtrace.join("\n")) if backtrace&.any?
|
|
151
|
-
rescue StandardError
|
|
152
|
-
# Never allow logging failures to interfere with request handling.
|
|
153
|
-
nil
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Locate the innermost logger that responds to `error`.
|
|
157
|
-
# @return [Object, nil]
|
|
158
|
-
def _verikloak_base_logger
|
|
159
|
-
root_logger = if defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
160
|
-
::Rails.logger
|
|
161
|
-
elsif respond_to?(:logger)
|
|
162
|
-
logger
|
|
163
|
-
end
|
|
164
|
-
current = root_logger
|
|
165
|
-
seen = Set.new
|
|
166
|
-
while current.respond_to?(:logger)
|
|
167
|
-
break unless seen.add?(current.object_id)
|
|
168
|
-
|
|
169
|
-
next_logger = current.logger
|
|
170
|
-
break if next_logger.nil? || next_logger.equal?(current)
|
|
171
|
-
|
|
172
|
-
current = next_logger
|
|
173
|
-
end
|
|
174
|
-
current
|
|
175
|
-
end
|
|
176
146
|
end
|
|
177
147
|
end
|
|
178
148
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
|
5
|
+
require 'active_support/core_ext/object/blank'
|
|
3
6
|
require 'rails/railtie'
|
|
4
7
|
require 'verikloak/middleware'
|
|
5
8
|
require_relative 'railtie_logger'
|
|
6
9
|
require_relative 'bff_configurator'
|
|
10
|
+
require_relative 'request_store_mirror'
|
|
7
11
|
|
|
8
12
|
module Verikloak
|
|
9
13
|
module Rails
|
|
@@ -20,7 +24,7 @@ module Verikloak
|
|
|
20
24
|
middleware_insert_after auto_insert_bff_header_guard
|
|
21
25
|
bff_header_guard_insert_before bff_header_guard_insert_after
|
|
22
26
|
token_verify_options decoder_cache_limit token_env_key user_env_key
|
|
23
|
-
bff_header_guard_options allow_http
|
|
27
|
+
bff_header_guard_options allow_http jwks_refresh_interval
|
|
24
28
|
].freeze
|
|
25
29
|
|
|
26
30
|
config.verikloak = ActiveSupport::OrderedOptions.new
|
|
@@ -35,8 +39,13 @@ module Verikloak
|
|
|
35
39
|
# Optionally include the controller concern when ActionController loads.
|
|
36
40
|
# Supports both ActionController::Base and ActionController::API (API mode).
|
|
37
41
|
# Skips inclusion if the controller already includes the concern.
|
|
42
|
+
#
|
|
43
|
+
# Registered after `verikloak.configure` so that when ActionController
|
|
44
|
+
# is already loaded (and the on_load hook therefore fires immediately),
|
|
45
|
+
# the concern's include-time reads of `render_500_json` / `rescue_pundit`
|
|
46
|
+
# see the application's final configuration instead of defaults.
|
|
38
47
|
# @return [void]
|
|
39
|
-
initializer 'verikloak.controller' do |_app|
|
|
48
|
+
initializer 'verikloak.controller', after: 'verikloak.configure' do |_app|
|
|
40
49
|
%i[action_controller_base action_controller_api].each do |hook|
|
|
41
50
|
ActiveSupport.on_load(hook) do
|
|
42
51
|
next if include?(Verikloak::Rails::Controller) # Already included, skip
|
|
@@ -63,22 +72,33 @@ module Verikloak
|
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
stack = insert_base_middleware(app)
|
|
66
|
-
|
|
75
|
+
if stack
|
|
76
|
+
insert_request_store_mirror(stack)
|
|
77
|
+
BffConfigurator.configure_bff_guard(stack)
|
|
78
|
+
end
|
|
67
79
|
|
|
68
80
|
stack
|
|
69
81
|
end
|
|
70
82
|
|
|
71
|
-
# Check if discovery_url is
|
|
83
|
+
# Check if discovery_url is configured (non-blank).
|
|
72
84
|
#
|
|
73
|
-
# @return [Boolean]
|
|
85
|
+
# @return [Boolean]
|
|
74
86
|
def discovery_url_present?
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
Verikloak::Rails.config.discovery_url.present?
|
|
88
|
+
end
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
# Mirror Verikloak env values into RequestStore when the gem is
|
|
91
|
+
# present, so the controller helpers' RequestStore fallback works
|
|
92
|
+
# outside the request cycle (e.g. jobs enqueued during a request).
|
|
93
|
+
#
|
|
94
|
+
# @param stack [ActionDispatch::MiddlewareStackProxy]
|
|
95
|
+
# @return [void]
|
|
96
|
+
def insert_request_store_mirror(stack)
|
|
97
|
+
return unless defined?(::RequestStore)
|
|
80
98
|
|
|
81
|
-
|
|
99
|
+
stack.insert_after ::Verikloak::Middleware, RequestStoreMirror
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
RailtieLogger.warn("[verikloak] Unable to insert RequestStoreMirror: #{e.message}")
|
|
82
102
|
end
|
|
83
103
|
|
|
84
104
|
# Log a warning message when discovery_url is missing.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'railtie_logger'
|
|
4
|
+
|
|
5
|
+
module Verikloak
|
|
6
|
+
module Rails
|
|
7
|
+
# Rack middleware that mirrors the Verikloak request context from the
|
|
8
|
+
# Rack env into +RequestStore+, so code running outside the controller
|
|
9
|
+
# (service objects, jobs enqueued during the request) can read the same
|
|
10
|
+
# values through the controller helpers' RequestStore fallback.
|
|
11
|
+
#
|
|
12
|
+
# The Railtie inserts this middleware right after +Verikloak::Middleware+
|
|
13
|
+
# and only when the +request_store+ gem is loaded. Values are written on
|
|
14
|
+
# every request (including +nil+ on skipped/unauthenticated paths) so no
|
|
15
|
+
# stale context leaks between requests even before request_store's own
|
|
16
|
+
# cleanup middleware runs. Applications that mirrored these keys by hand
|
|
17
|
+
# before this middleware existed should remove their own writer — it
|
|
18
|
+
# would otherwise be overwritten here.
|
|
19
|
+
#
|
|
20
|
+
# Mirroring is best-effort: a failure never breaks the request, but it
|
|
21
|
+
# is logged (once per middleware instance) so a silently dead fallback
|
|
22
|
+
# does not go unnoticed.
|
|
23
|
+
class RequestStoreMirror
|
|
24
|
+
# @param app [#call] next Rack application
|
|
25
|
+
def initialize(app)
|
|
26
|
+
@app = app
|
|
27
|
+
@failure_warned = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Mirror claims/token into RequestStore, then call downstream.
|
|
31
|
+
#
|
|
32
|
+
# @param env [Hash] Rack environment
|
|
33
|
+
# @return [Array] Rack response triple
|
|
34
|
+
def call(env)
|
|
35
|
+
mirror(env)
|
|
36
|
+
@app.call(env)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Best-effort mirroring; never breaks the request.
|
|
42
|
+
#
|
|
43
|
+
# @param env [Hash]
|
|
44
|
+
# @return [void]
|
|
45
|
+
def mirror(env)
|
|
46
|
+
return unless defined?(::RequestStore)
|
|
47
|
+
|
|
48
|
+
config = Verikloak::Rails.config
|
|
49
|
+
store = ::RequestStore.store
|
|
50
|
+
store[:verikloak_user] = env[config.effective_user_env_key]
|
|
51
|
+
store[:verikloak_token] = env[config.effective_token_env_key]
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
warn_mirror_failure(e)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Log the first mirroring failure so the RequestStore fallback does not
|
|
57
|
+
# die silently; stay quiet afterwards to avoid flooding the log on
|
|
58
|
+
# every request.
|
|
59
|
+
#
|
|
60
|
+
# @param error [StandardError]
|
|
61
|
+
# @return [void]
|
|
62
|
+
def warn_mirror_failure(error)
|
|
63
|
+
return if @failure_warned
|
|
64
|
+
|
|
65
|
+
@failure_warned = true
|
|
66
|
+
RailtieLogger.warn(
|
|
67
|
+
"[verikloak] RequestStoreMirror could not mirror the request context: #{error.class}: #{error.message}"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Rails
|
|
5
|
+
module Testing
|
|
6
|
+
# Builds JWT-shaped claim Hashes from a user-like object for use in
|
|
7
|
+
# tests. The returned Hash uses string keys to match what
|
|
8
|
+
# `Verikloak::Middleware` writes to `env['verikloak.user']` after a
|
|
9
|
+
# successful token verification.
|
|
10
|
+
#
|
|
11
|
+
# The user object is duck-typed: it must respond to `uid` and `email`.
|
|
12
|
+
# Optional methods used when present:
|
|
13
|
+
# - `username` or `preferred_username` (for `preferred_username`)
|
|
14
|
+
# - `first_name` (for `given_name`)
|
|
15
|
+
# - `last_name` (for `family_name`)
|
|
16
|
+
module ClaimsBuilder
|
|
17
|
+
# Build a baseline OIDC-style claim Hash.
|
|
18
|
+
#
|
|
19
|
+
# @param user [Object] user-like object responding to `uid`, `email`
|
|
20
|
+
# @param groups [Array<String>] values for the `groups` claim
|
|
21
|
+
# @param extra_claims [Hash] additional claims to merge in (overrides
|
|
22
|
+
# any keys produced from `user`/`groups`)
|
|
23
|
+
# @return [Hash{String=>Object}]
|
|
24
|
+
def build_jwt_claims(user, groups: [], extra_claims: {})
|
|
25
|
+
base = {
|
|
26
|
+
'sub' => user.uid,
|
|
27
|
+
'email' => user.email,
|
|
28
|
+
'preferred_username' => preferred_username_for(user),
|
|
29
|
+
'given_name' => safe_call(user, :first_name),
|
|
30
|
+
'family_name' => safe_call(user, :last_name),
|
|
31
|
+
'groups' => groups,
|
|
32
|
+
'realm_access' => { 'roles' => [] },
|
|
33
|
+
'resource_access' => {},
|
|
34
|
+
'aud' => ['account']
|
|
35
|
+
}.compact
|
|
36
|
+
|
|
37
|
+
base.merge(stringify_keys(extra_claims))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Convenience wrapper that assigns the configured admin group.
|
|
41
|
+
#
|
|
42
|
+
# @param user [Object]
|
|
43
|
+
# @param admin_group [String] group identifier (default: "/admin")
|
|
44
|
+
# @param extra_claims [Hash]
|
|
45
|
+
# @return [Hash{String=>Object}]
|
|
46
|
+
def build_admin_claims(user, admin_group: '/admin', extra_claims: {})
|
|
47
|
+
build_jwt_claims(user, groups: [admin_group], extra_claims: extra_claims)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convenience wrapper that assigns the configured user group.
|
|
51
|
+
#
|
|
52
|
+
# @param user [Object]
|
|
53
|
+
# @param user_group [String] group identifier (default: "/user")
|
|
54
|
+
# @param extra_claims [Hash]
|
|
55
|
+
# @return [Hash{String=>Object}]
|
|
56
|
+
def build_user_claims(user, user_group: '/user', extra_claims: {})
|
|
57
|
+
build_jwt_claims(user, groups: [user_group], extra_claims: extra_claims)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def preferred_username_for(user)
|
|
63
|
+
candidate = if user.respond_to?(:username)
|
|
64
|
+
user.username
|
|
65
|
+
elsif user.respond_to?(:preferred_username)
|
|
66
|
+
user.preferred_username
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Fall back to email when the configured method exists but
|
|
70
|
+
# returns a blank value (nil/empty), so the
|
|
71
|
+
# `preferred_username` claim is always populated when the user
|
|
72
|
+
# has at least an email address.
|
|
73
|
+
present?(candidate) ? candidate : user.email
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def present?(value)
|
|
77
|
+
return false if value.nil?
|
|
78
|
+
return false if value.respond_to?(:empty?) && value.empty?
|
|
79
|
+
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def safe_call(user, method)
|
|
84
|
+
user.public_send(method) if user.respond_to?(method)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stringify_keys(hash)
|
|
88
|
+
return {} unless hash.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
hash.transform_keys(&:to_s)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'verikloak/rails/testing/claims_builder'
|
|
4
|
+
require 'verikloak/rails/testing/middleware_stub'
|
|
5
|
+
|
|
6
|
+
module Verikloak
|
|
7
|
+
module Rails
|
|
8
|
+
module Testing
|
|
9
|
+
# Top-level mix-in for RSpec example groups (request and policy specs).
|
|
10
|
+
# Composes {ClaimsBuilder} and {MiddlewareStub}, and adds
|
|
11
|
+
# `Verikloak::Pundit::UserContext` builders when the optional
|
|
12
|
+
# `verikloak-pundit` gem is loaded.
|
|
13
|
+
module Helpers
|
|
14
|
+
include ClaimsBuilder
|
|
15
|
+
include MiddlewareStub
|
|
16
|
+
|
|
17
|
+
# Build a `Verikloak::Pundit::UserContext` for policy specs.
|
|
18
|
+
#
|
|
19
|
+
# Matches verikloak-pundit's constructor
|
|
20
|
+
# (`UserContext.new(claims, resource_client: nil, config: nil)`),
|
|
21
|
+
# which wraps the JWT claims only — the application user object is
|
|
22
|
+
# not part of the context.
|
|
23
|
+
#
|
|
24
|
+
# @param claims [Hash] JWT claims (string keys)
|
|
25
|
+
# @param options [Hash] keyword options forwarded to `UserContext.new`
|
|
26
|
+
# (e.g. `resource_client:`, `config:`)
|
|
27
|
+
# @return [Verikloak::Pundit::UserContext]
|
|
28
|
+
# @raise [RuntimeError] if `verikloak-pundit` is not loaded
|
|
29
|
+
def build_pundit_user_context(claims, **options)
|
|
30
|
+
unless defined?(::Verikloak::Pundit::UserContext)
|
|
31
|
+
raise 'verikloak-pundit gem is not loaded; cannot build a UserContext'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
::Verikloak::Pundit::UserContext.new(claims, **options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convenience wrapper: admin claims + UserContext.
|
|
38
|
+
#
|
|
39
|
+
# @param user [Object] user-like object used to build the claims
|
|
40
|
+
# @param admin_group [String]
|
|
41
|
+
# @param options [Hash] forwarded to {#build_pundit_user_context}
|
|
42
|
+
# @return [Verikloak::Pundit::UserContext]
|
|
43
|
+
def build_admin_user_context(user, admin_group: '/admin', **options)
|
|
44
|
+
build_pundit_user_context(build_admin_claims(user, admin_group: admin_group), **options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Convenience wrapper: user claims + UserContext.
|
|
48
|
+
#
|
|
49
|
+
# @param user [Object] user-like object used to build the claims
|
|
50
|
+
# @param user_group [String]
|
|
51
|
+
# @param options [Hash] forwarded to {#build_pundit_user_context}
|
|
52
|
+
# @return [Verikloak::Pundit::UserContext]
|
|
53
|
+
def build_user_user_context(user, user_group: '/user', **options)
|
|
54
|
+
build_pundit_user_context(build_user_claims(user, user_group: user_group), **options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Rails
|
|
5
|
+
module Testing
|
|
6
|
+
# Stubs the Verikloak middleware stack so authenticated request specs
|
|
7
|
+
# can run without contacting an OIDC provider or signing real JWTs.
|
|
8
|
+
#
|
|
9
|
+
# `stub_verikloak_middleware` patches `#call` on the relevant
|
|
10
|
+
# middlewares so they inject the supplied claims into
|
|
11
|
+
# `env['verikloak.user']` and pass through to the next middleware.
|
|
12
|
+
# The companion `verikloak.token` env key is also set when
|
|
13
|
+
# available so controller helpers like `current_token` work.
|
|
14
|
+
# Custom `user_env_key` / `token_env_key` settings on
|
|
15
|
+
# `Verikloak::Rails.config` are honored automatically.
|
|
16
|
+
#
|
|
17
|
+
# Requires RSpec-style mocks (`allow_any_instance_of`). Load
|
|
18
|
+
# `verikloak/rails/testing/rspec` to wire things up automatically,
|
|
19
|
+
# or include this module directly into your example groups.
|
|
20
|
+
module MiddlewareStub
|
|
21
|
+
DEFAULT_STUB_TOKEN = 'verikloak-test-token'
|
|
22
|
+
|
|
23
|
+
# Stub all loaded Verikloak middlewares to populate the request
|
|
24
|
+
# environment with the supplied claims.
|
|
25
|
+
#
|
|
26
|
+
# @param claims [Hash] value placed into `env['verikloak.user']`
|
|
27
|
+
# @param token [String] value placed into `env['verikloak.token']`
|
|
28
|
+
# @return [void]
|
|
29
|
+
def stub_verikloak_middleware(claims, token: DEFAULT_STUB_TOKEN)
|
|
30
|
+
stub_core_middleware(claims, token)
|
|
31
|
+
stub_bff_middleware(claims, token) if bff_middleware_loaded?
|
|
32
|
+
stub_audience_middleware(claims, token) if audience_middleware_loaded?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def stub_core_middleware(claims, token)
|
|
38
|
+
return unless defined?(::Verikloak::Middleware)
|
|
39
|
+
|
|
40
|
+
install_passthrough_stub(::Verikloak::Middleware, claims, token)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stub_bff_middleware(claims, token)
|
|
44
|
+
install_passthrough_stub(::Verikloak::BFF::HeaderGuard, claims, token)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stub_audience_middleware(claims, token)
|
|
48
|
+
install_passthrough_stub(::Verikloak::Audience::Middleware, claims, token)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def install_passthrough_stub(middleware_class, claims, token)
|
|
52
|
+
# Env keys are resolved inside the stubbed call — i.e. when the
|
|
53
|
+
# request runs, not when the stub is installed — so configuration
|
|
54
|
+
# applied after `stub_verikloak_middleware` (an inner context or a
|
|
55
|
+
# lazily booted app setting custom env keys) still lines up with
|
|
56
|
+
# the controller helpers' request-time key resolution.
|
|
57
|
+
allow_any_instance_of(middleware_class).to receive(:call) do |instance, env|
|
|
58
|
+
env[stub_user_env_key] = claims
|
|
59
|
+
env[stub_token_env_key] = token if token
|
|
60
|
+
inner_app = instance.instance_variable_get(:@app)
|
|
61
|
+
inner_app.call(env)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolve env keys from the live configuration so the stub follows
|
|
66
|
+
# custom `user_env_key` / `token_env_key` settings. Falls back to the
|
|
67
|
+
# core defaults when verikloak-rails is not fully loaded.
|
|
68
|
+
def stub_user_env_key
|
|
69
|
+
return ::Verikloak::Rails.config.effective_user_env_key if verikloak_rails_config?
|
|
70
|
+
|
|
71
|
+
'verikloak.user'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stub_token_env_key
|
|
75
|
+
return ::Verikloak::Rails.config.effective_token_env_key if verikloak_rails_config?
|
|
76
|
+
|
|
77
|
+
'verikloak.token'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def verikloak_rails_config?
|
|
81
|
+
::Verikloak::Rails.respond_to?(:config) &&
|
|
82
|
+
::Verikloak::Rails.config.respond_to?(:effective_user_env_key)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def bff_middleware_loaded?
|
|
86
|
+
defined?(::Verikloak::BFF::HeaderGuard)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def audience_middleware_loaded?
|
|
90
|
+
defined?(::Verikloak::Audience::Middleware)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RSpec integration for Verikloak::Rails::Testing.
|
|
4
|
+
#
|
|
5
|
+
# Require this file from `spec/rails_helper.rb` (or `spec/spec_helper.rb`)
|
|
6
|
+
# to:
|
|
7
|
+
#
|
|
8
|
+
# 1. Mix {Verikloak::Rails::Testing::Helpers} into request and policy
|
|
9
|
+
# specs. Controller specs (`type: :controller`) are intentionally
|
|
10
|
+
# excluded because they bypass the Rack middleware stack, which is
|
|
11
|
+
# where `stub_verikloak_middleware` injects claims; use a request
|
|
12
|
+
# spec instead, or set `request.env['verikloak.user']` directly when
|
|
13
|
+
# you must use a controller spec.
|
|
14
|
+
# 2. Register the shared contexts:
|
|
15
|
+
# - `"with verikloak admin auth"`
|
|
16
|
+
# - `"with verikloak user auth"`
|
|
17
|
+
# - `"with verikloak custom auth"`
|
|
18
|
+
#
|
|
19
|
+
# The shared contexts assume a `current_user` factory exists in the host
|
|
20
|
+
# application (e.g. `create(:user)`). Override `let(:current_user)` to
|
|
21
|
+
# inject a different user object.
|
|
22
|
+
|
|
23
|
+
require 'verikloak/rails/testing/helpers'
|
|
24
|
+
|
|
25
|
+
raise 'verikloak/rails/testing/rspec requires RSpec' unless defined?(RSpec)
|
|
26
|
+
|
|
27
|
+
RSpec.configure do |config|
|
|
28
|
+
config.include Verikloak::Rails::Testing::Helpers, type: :request
|
|
29
|
+
config.include Verikloak::Rails::Testing::Helpers, type: :policy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
RSpec.shared_context 'with verikloak admin auth' do
|
|
33
|
+
let(:current_user) { create(:user) }
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
stub_verikloak_middleware(build_admin_claims(current_user))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
RSpec.shared_context 'with verikloak user auth' do
|
|
41
|
+
let(:current_user) { create(:user) }
|
|
42
|
+
|
|
43
|
+
before do
|
|
44
|
+
stub_verikloak_middleware(build_user_claims(current_user))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
RSpec.shared_context 'with verikloak custom auth' do
|
|
49
|
+
let(:current_user) { create(:user) }
|
|
50
|
+
let(:verikloak_groups) { [] }
|
|
51
|
+
let(:verikloak_extra_claims) { {} }
|
|
52
|
+
|
|
53
|
+
before do
|
|
54
|
+
stub_verikloak_middleware(
|
|
55
|
+
build_jwt_claims(current_user, groups: verikloak_groups, extra_claims: verikloak_extra_claims)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verikloak
|
|
4
|
+
module Rails
|
|
5
|
+
# Test-support helpers for application specs that exercise endpoints
|
|
6
|
+
# protected by Verikloak.
|
|
7
|
+
#
|
|
8
|
+
# Submodules:
|
|
9
|
+
# - {ClaimsBuilder} – build JWT-shaped Hashes from a user-like
|
|
10
|
+
# object. Pure Ruby, no test-framework dependency, so it can be
|
|
11
|
+
# used standalone (e.g. from `Minitest`).
|
|
12
|
+
# - {MiddlewareStub} – stub Verikloak/BFF/Audience middleware to
|
|
13
|
+
# inject pre-built claims into `env['verikloak.user']`. Requires
|
|
14
|
+
# RSpec mocks (`allow_any_instance_of`); not usable from
|
|
15
|
+
# `Minitest` without bringing in `rspec-mocks`.
|
|
16
|
+
# - {Helpers} – mixes in {ClaimsBuilder} and {MiddlewareStub}
|
|
17
|
+
# and adds Pundit `UserContext` builders when `verikloak-pundit`
|
|
18
|
+
# is loaded.
|
|
19
|
+
#
|
|
20
|
+
# The simplest way to wire these into an RSpec suite is to require
|
|
21
|
+
# `verikloak/rails/testing/rspec` from `spec/rails_helper.rb`.
|
|
22
|
+
module Testing
|
|
23
|
+
autoload :ClaimsBuilder, 'verikloak/rails/testing/claims_builder'
|
|
24
|
+
autoload :MiddlewareStub, 'verikloak/rails/testing/middleware_stub'
|
|
25
|
+
autoload :Helpers, 'verikloak/rails/testing/helpers'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/verikloak/rails.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'verikloak/rails/skip_path_checker'
|
|
|
5
5
|
require 'verikloak/rails/configuration'
|
|
6
6
|
require 'verikloak/rails/error_renderer'
|
|
7
7
|
require 'verikloak/rails/controller'
|
|
8
|
+
require 'verikloak/rails/request_store_mirror'
|
|
8
9
|
require 'verikloak/rails/railtie'
|
|
9
10
|
|
|
10
11
|
module Verikloak
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: verikloak-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -55,14 +55,14 @@ dependencies:
|
|
|
55
55
|
requirements:
|
|
56
56
|
- - "~>"
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: '1.
|
|
58
|
+
version: '1.1'
|
|
59
59
|
type: :runtime
|
|
60
60
|
prerelease: false
|
|
61
61
|
version_requirements: !ruby/object:Gem::Requirement
|
|
62
62
|
requirements:
|
|
63
63
|
- - "~>"
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
|
-
version: '1.
|
|
65
|
+
version: '1.1'
|
|
66
66
|
description: 'Rails integration for Verikloak: auto middleware, helpers, and standardized
|
|
67
67
|
JSON errors.'
|
|
68
68
|
executables: []
|
|
@@ -79,10 +79,17 @@ files:
|
|
|
79
79
|
- lib/verikloak/rails/bff_configurator.rb
|
|
80
80
|
- lib/verikloak/rails/configuration.rb
|
|
81
81
|
- lib/verikloak/rails/controller.rb
|
|
82
|
+
- lib/verikloak/rails/controller/error_handling.rb
|
|
82
83
|
- lib/verikloak/rails/error_renderer.rb
|
|
83
84
|
- lib/verikloak/rails/railtie.rb
|
|
84
85
|
- lib/verikloak/rails/railtie_logger.rb
|
|
86
|
+
- lib/verikloak/rails/request_store_mirror.rb
|
|
85
87
|
- lib/verikloak/rails/skip_path_checker.rb
|
|
88
|
+
- lib/verikloak/rails/testing.rb
|
|
89
|
+
- lib/verikloak/rails/testing/claims_builder.rb
|
|
90
|
+
- lib/verikloak/rails/testing/helpers.rb
|
|
91
|
+
- lib/verikloak/rails/testing/middleware_stub.rb
|
|
92
|
+
- lib/verikloak/rails/testing/rspec.rb
|
|
86
93
|
- lib/verikloak/rails/version.rb
|
|
87
94
|
homepage: https://github.com/taiyaky/verikloak-rails
|
|
88
95
|
licenses:
|
|
@@ -91,7 +98,7 @@ metadata:
|
|
|
91
98
|
source_code_uri: https://github.com/taiyaky/verikloak-rails
|
|
92
99
|
changelog_uri: https://github.com/taiyaky/verikloak-rails/blob/main/CHANGELOG.md
|
|
93
100
|
bug_tracker_uri: https://github.com/taiyaky/verikloak-rails/issues
|
|
94
|
-
documentation_uri: https://rubydoc.info/gems/verikloak-rails/1.0
|
|
101
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-rails/1.2.0
|
|
95
102
|
rubygems_mfa_required: 'true'
|
|
96
103
|
rdoc_options: []
|
|
97
104
|
require_paths:
|