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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbb1d62264480e1ed9e4f045df4d1cd694e1ed7fdc2240c815d98ecd5dd39b33
4
- data.tar.gz: 94bbb11fadae1cb555be97cc4715bdf357cf9252464da6b303bf4fb98ea3d77c
3
+ metadata.gz: 4ab889b6413e152dc2be695f2f4f738d7e2b90712b54559cc596e6d1c92166ed
4
+ data.tar.gz: b97dd2c56aca7b021daf781ffc2ad834bc0b42c4b72efdd9cdda1d609495f4a6
5
5
  SHA512:
6
- metadata.gz: 62c094a2c975973123e36f50aba4ccbbb2fc58fb82de69f3ad8b40a2363051fc02b1119fbb393854a3bb7faba30b2618035340aee1bf8cc95620ce0e4cdbe5c1
7
- data.tar.gz: 24626f36ab835977055470020cc9371f7d76217a59f4907010a9f2d7761ece802a0ea4f3a57497306e543ffa369ad9ee95f32a8c41c8f027b25ad8c871e0c33e
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 background jobs
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
- # In a background job triggered during request
66
- # (when RequestStore gem is present and middleware has mirrored values)
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
- require 'set'
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
- # Register generic error handler first so specific handlers take precedence.
19
- if Verikloak::Rails.config.render_500_json
20
- rescue_from StandardError do |e|
21
- _verikloak_log_internal_error(e)
22
- render json: { error: 'internal_server_error', message: 'An unexpected error occurred' },
23
- status: :internal_server_error
24
- end
25
- end
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 do |e|
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 RequestStore when available.
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('verikloak.user', :verikloak_user)
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 RequestStore when available.
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('verikloak.token', :verikloak_token)
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
- rid = rid.to_s.gsub(/[[:cntrl:]]+/, ' ').strip
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
- BffConfigurator.configure_bff_guard(stack) if stack
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 present and valid.
83
+ # Check if discovery_url is configured (non-blank).
72
84
  #
73
- # @return [Boolean] true if discovery_url is configured and not empty
85
+ # @return [Boolean]
74
86
  def discovery_url_present?
75
- discovery_url = Verikloak::Rails.config.discovery_url
76
- return false unless discovery_url
87
+ Verikloak::Rails.config.discovery_url.present?
88
+ end
77
89
 
78
- return !discovery_url.blank? if discovery_url.respond_to?(:blank?)
79
- return !discovery_url.empty? if discovery_url.respond_to?(:empty?)
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
- true
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Verikloak
4
4
  module Rails
5
- VERSION = '1.0.1'
5
+ VERSION = '1.2.0'
6
6
  end
7
7
  end
@@ -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.1
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.0'
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.0'
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.1
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: