verikloak-rails 0.2.7 → 0.2.8

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: d96aa5ed90bc1dbc922ec263fecbccdb8dd122e51accced8776a969211e4f549
4
- data.tar.gz: d7cc87f1b7cf857fbc707962860a596f35efa32f167c76e1bf50362090f6d5b6
3
+ metadata.gz: cbc6aebdf72129720e402e9e90f168b311fa9718c704c86f67d68d9a560cf706
4
+ data.tar.gz: 10c488b6ac7dbe7c2e2951300f2a4f1b21ccfe3ea0f9517ec2537aee6e6fb0a1
5
5
  SHA512:
6
- metadata.gz: 518cf212cede738ce3e392a93f413b654ba138d4768afbe20811274129f93139aef1afbe5c24cbf3309cf7d99c978cf9a4ff6d8443b01cc7dae543da97f04c7c
7
- data.tar.gz: 412639b3aa790ffae71cf53dc427e213ba5b9ae426270cbc6af3e1e0ce26c028abc98db14b0bce58153601f10a7b86c566b4ffdbfd7dcd954175008a6d146025
6
+ metadata.gz: 50bf90d8935e1a402c71075f015f3fc113894171129ba510ef91164614cb636b99474ddb3ab7c18300d1c59d32f44de1d58e8fe8b2c3dc084419e65170094c09
7
+ data.tar.gz: 2e2510548db798e6295a4d36b0f655c4c802908a1842e82bb4680a814d5847de1c4eab60f20cb3d7219e24023c48b64a7ff745f3901de913fb4a7f4ac5808073
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.8] - 2025-01-03
11
+
12
+ ### Changed
13
+ - Refactor controller helpers to use shared `_verikloak_fetch_request_context` method for consistent RequestStore fallback handling
14
+ - Optimize configuration access by caching `Verikloak::Rails.config` in log tag building
15
+ - Extract `auth_headers` method in ErrorRenderer for reusable WWW-Authenticate header generation
16
+ - Streamline middleware configuration by inlining BFF guard insertion into main configuration flow
17
+ - Improve middleware insertion error handling with `try_insert_after` and `warn_with_fallback` utilities
18
+
19
+ ### Added
20
+ - Support for advanced middleware options: `token_verify_options`, `decoder_cache_limit`, `token_env_key`, `user_env_key`
21
+ - `bff_header_guard_options` configuration for verikloak-bff library integration
22
+ - Centralized `CONFIG_KEYS` constant in Railtie for consistent configuration management
23
+ - Enhanced README documentation with RequestStore vs Rack environment priority examples
24
+
25
+ ### Fixed
26
+ - Only call `configure_bff_guard` when middleware stack is successfully created
27
+ - Replace `each_with_object` with `transform_keys` for better Ruby style compliance
28
+
10
29
  ## [0.2.7] - 2025-09-23
11
30
 
12
31
  ### Fixed
data/README.md CHANGED
@@ -48,6 +48,33 @@ Then configure `config/initializers/verikloak.rb`.
48
48
  | `current_user_claims` | `verikloak.user` | `:verikloak_user` | Uses RequestStore only when available |
49
49
  | `current_token` | `verikloak.token` | `:verikloak_token` | Uses RequestStore only when available |
50
50
 
51
+ #### Priority and Behavior Details
52
+
53
+ The helpers follow this priority order:
54
+
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
57
+
58
+ **Examples:**
59
+
60
+ ```ruby
61
+ # In a controller action (normal case)
62
+ current_user_claims # reads from request.env['verikloak.user']
63
+ current_token # reads from request.env['verikloak.token']
64
+
65
+ # In a background job triggered during request
66
+ # (when RequestStore gem is present and middleware has mirrored values)
67
+ current_user_claims # falls back to RequestStore.store[:verikloak_user]
68
+ current_token # falls back to RequestStore.store[:verikloak_token]
69
+
70
+ # When RequestStore is not available or disabled
71
+ current_user_claims # returns nil if not in request.env
72
+ current_token # returns nil if not in request.env
73
+ ```
74
+
75
+ **Custom Environment Keys**: If you configure custom `token_env_key` or `user_env_key`,
76
+ the helpers automatically adapt to use those keys instead of the defaults.
77
+
51
78
  ### Example Controller
52
79
 
53
80
  ```ruby
@@ -99,6 +126,12 @@ When `verikloak-bff` is on the load path, `verikloak-rails` automatically insert
99
126
 
100
127
  Use verikloak-bff alongside this gem when you front Rails with a BFF/proxy such as oauth2-proxy and need to enforce trusted forwarding and header consistency.
101
128
 
129
+ Assign `config.verikloak.bff_header_guard_options` to customize the guard before
130
+ it is inserted. Provide either a Hash (merged via attribute writers) or a block/
131
+ callable that receives the `Verikloak::BFF.configure` object so you can set
132
+ advanced options such as trusted proxies, forwarded header names, or custom log
133
+ hooks directly from the Rails initializer.
134
+
102
135
  ## Configuration (initializer)
103
136
  ### Keys
104
137
  Keys under `config.verikloak`:
@@ -120,6 +153,11 @@ Keys under `config.verikloak`:
120
153
  | `auto_insert_bff_header_guard` | Boolean | Auto insert `Verikloak::Bff::HeaderGuard` when the gem is present | `true` |
121
154
  | `bff_header_guard_insert_before` | Object/String/Symbol | Insert the header guard before this middleware (`Verikloak::Middleware` when `nil`) | `nil` |
122
155
  | `bff_header_guard_insert_after` | Object/String/Symbol | Insert the header guard after this middleware | `nil` |
156
+ | `token_verify_options` | Hash | Additional options forwarded to `Verikloak::TokenDecoder` (e.g. `{ verify_iat: false }`) | `{}` |
157
+ | `decoder_cache_limit` | Integer or nil | Overrides the cached decoder count before eviction | `nil` (verikloak default `128`) |
158
+ | `token_env_key` | String | Custom Rack env key that stores the Bearer token | `nil` (middleware default `verikloak.token`) |
159
+ | `user_env_key` | String | Custom Rack env key that stores decoded claims | `nil` (middleware default `verikloak.user`) |
160
+ | `bff_header_guard_options` | Hash or Proc | Forwarded to `Verikloak::BFF.configure` prior to middleware insertion | `{}` |
123
161
 
124
162
  Environment variable examples are in the generated initializer.
125
163
 
@@ -59,7 +59,9 @@ module Verikloak
59
59
  :render_500_json, :rescue_pundit,
60
60
  :middleware_insert_before, :middleware_insert_after,
61
61
  :auto_insert_bff_header_guard,
62
- :bff_header_guard_insert_before, :bff_header_guard_insert_after
62
+ :bff_header_guard_insert_before, :bff_header_guard_insert_after,
63
+ :token_verify_options, :decoder_cache_limit,
64
+ :token_env_key, :user_env_key, :bff_header_guard_options
63
65
 
64
66
  # Initialize configuration with sensible defaults for Rails apps.
65
67
  # @return [void]
@@ -79,6 +81,11 @@ module Verikloak
79
81
  @auto_insert_bff_header_guard = true
80
82
  @bff_header_guard_insert_before = nil
81
83
  @bff_header_guard_insert_after = nil
84
+ @token_verify_options = {}
85
+ @decoder_cache_limit = nil
86
+ @token_env_key = nil
87
+ @user_env_key = nil
88
+ @bff_header_guard_options = {}
82
89
  end
83
90
 
84
91
  # Options forwarded to the base Verikloak Rack middleware.
@@ -92,7 +99,11 @@ module Verikloak
92
99
  audience: audience,
93
100
  issuer: issuer,
94
101
  leeway: leeway,
95
- skip_paths: skip_paths
102
+ skip_paths: skip_paths,
103
+ token_verify_options: token_verify_options,
104
+ decoder_cache_limit: decoder_cache_limit,
105
+ token_env_key: token_env_key,
106
+ user_env_key: user_env_key
96
107
  }.compact
97
108
  end
98
109
  end
@@ -59,22 +59,14 @@ module Verikloak
59
59
  # Prefer Rack env; fall back to RequestStore when available.
60
60
  # @return [Hash, nil]
61
61
  def current_user_claims
62
- env_claims = request.env['verikloak.user']
63
- return env_claims unless env_claims.nil?
64
- return ::RequestStore.store[:verikloak_user] if defined?(::RequestStore) && ::RequestStore.respond_to?(:store)
65
-
66
- nil
62
+ _verikloak_fetch_request_context('verikloak.user', :verikloak_user)
67
63
  end
68
64
 
69
65
  # The raw bearer token used for the current request.
70
66
  # Prefer Rack env; fall back to RequestStore when available.
71
67
  # @return [String, nil]
72
68
  def current_token
73
- env_token = request.env['verikloak.token']
74
- return env_token unless env_token.nil?
75
- return ::RequestStore.store[:verikloak_token] if defined?(::RequestStore) && ::RequestStore.respond_to?(:store)
76
-
77
- nil
69
+ _verikloak_fetch_request_context('verikloak.token', :verikloak_token)
78
70
  end
79
71
 
80
72
  # The `sub` (subject) claim from the current user claims.
@@ -112,13 +104,14 @@ module Verikloak
112
104
  # Build log tags from request context with minimal branching and safe values.
113
105
  # @return [Array<String>]
114
106
  def _verikloak_build_log_tags
107
+ config = Verikloak::Rails.config
115
108
  tags = []
116
- if Verikloak::Rails.config.logger_tags.include?(:request_id)
109
+ if config.logger_tags.include?(:request_id)
117
110
  rid = request.request_id || request.headers['X-Request-Id']
118
111
  rid = rid.to_s.gsub(/[\r\n]+/, ' ')
119
112
  tags << "req:#{rid}" unless rid.empty?
120
113
  end
121
- if Verikloak::Rails.config.logger_tags.include?(:sub)
114
+ if config.logger_tags.include?(:sub)
122
115
  sub = current_subject
123
116
  if sub
124
117
  sanitized = sub.to_s.gsub(/[[:cntrl:]]+/, ' ').strip
@@ -128,6 +121,21 @@ module Verikloak
128
121
  tags
129
122
  end
130
123
 
124
+ # Retrieve request context from Rack env or RequestStore.
125
+ # @param env_key [String]
126
+ # @param store_key [Symbol]
127
+ # @return [Object, nil]
128
+ def _verikloak_fetch_request_context(env_key, store_key)
129
+ env_value = request.env[env_key]
130
+ return env_value unless env_value.nil?
131
+ return unless defined?(::RequestStore) && ::RequestStore.respond_to?(:store)
132
+
133
+ store = ::RequestStore.store
134
+ return unless store.respond_to?(:[])
135
+
136
+ store[store_key]
137
+ end
138
+
131
139
  # Write StandardError details to the controller or Rails logger when
132
140
  # rendering the generic 500 JSON response. Logging ensures the
133
141
  # underlying failure is still visible to operators even though the
@@ -34,14 +34,9 @@ module Verikloak
34
34
  def render(controller, error)
35
35
  code, message = extract_code_message(error)
36
36
  status = status_for(error, code)
37
- headers = {}
38
- if status == 401
39
- hdr = +'Bearer'
40
- hdr << %( error="#{sanitize_quoted(code)}") if code
41
- hdr << %( error_description="#{sanitize_quoted(message)}") if message
42
- headers['WWW-Authenticate'] = hdr
37
+ auth_headers(status, code, message).each do |header, value|
38
+ controller.response.set_header(header, value)
43
39
  end
44
- headers.each { |k, v| controller.response.set_header(k, v) }
45
40
  controller.render json: { error: code || 'unauthorized', message: message }, status: status
46
41
  end
47
42
 
@@ -71,6 +66,20 @@ module Verikloak
71
66
  end
72
67
  end
73
68
 
69
+ # Build WWW-Authenticate headers when returning 401 responses.
70
+ # @param status [Integer]
71
+ # @param code [String, nil]
72
+ # @param message [String]
73
+ # @return [Hash<String, String>]
74
+ def auth_headers(status, code, message)
75
+ return {} unless status == 401
76
+
77
+ header = +'Bearer'
78
+ header << %( error="#{sanitize_quoted(code)}") if code
79
+ header << %( error_description="#{sanitize_quoted(message)}") if message
80
+ { 'WWW-Authenticate' => header }
81
+ end
82
+
74
83
  # Sanitize a value for inclusion inside a quoted HTTP header parameter.
75
84
  # Escapes quotes and backslashes, and strips CR/LF to prevent header injection.
76
85
  # Why block replacement? String replacements like '\\1' are parsed as
@@ -11,13 +11,22 @@ module Verikloak
11
11
  # - Inserts base `Verikloak::Middleware`
12
12
  # - Auto-includes controller concern when enabled
13
13
  class Railtie < ::Rails::Railtie
14
+ CONFIG_KEYS = %i[
15
+ discovery_url audience issuer leeway skip_paths
16
+ logger_tags error_renderer auto_include_controller
17
+ render_500_json rescue_pundit middleware_insert_before
18
+ middleware_insert_after auto_insert_bff_header_guard
19
+ bff_header_guard_insert_before bff_header_guard_insert_after
20
+ token_verify_options decoder_cache_limit token_env_key user_env_key
21
+ bff_header_guard_options
22
+ ].freeze
23
+
14
24
  config.verikloak = ActiveSupport::OrderedOptions.new
15
25
 
16
26
  # Apply configuration and insert middleware.
17
27
  # @return [void]
18
28
  initializer 'verikloak.configure' do |app|
19
- stack = ::Verikloak::Rails::Railtie.send(:configure_middleware, app)
20
- ::Verikloak::Rails::Railtie.send(:configure_bff_guard, stack) if stack
29
+ ::Verikloak::Rails::Railtie.send(:configure_middleware, app)
21
30
  end
22
31
 
23
32
  # Optionally include the controller concern when ActionController loads.
@@ -37,13 +46,17 @@ module Verikloak
37
46
  # @return [ActionDispatch::MiddlewareStackProxy] configured middleware stack
38
47
  def configure_middleware(app)
39
48
  apply_configuration(app)
49
+ configure_bff_library
40
50
 
41
51
  unless discovery_url_present?
42
52
  log_missing_discovery_url_warning
43
53
  return
44
54
  end
45
55
 
46
- insert_base_middleware(app)
56
+ stack = insert_base_middleware(app)
57
+ configure_bff_guard(stack) if stack
58
+
59
+ stack
47
60
  end
48
61
 
49
62
  # Insert the optional HeaderGuard middleware when verikloak-bff is present.
@@ -65,6 +78,33 @@ module Verikloak
65
78
  end
66
79
  end
67
80
 
81
+ # Apply configuration options to the verikloak-bff namespace.
82
+ # Supports hash-like and callable inputs.
83
+ #
84
+ # @param target [Module] Verikloak::BFF or Verikloak::Bff namespace
85
+ # @param options [Hash, Proc, #to_h]
86
+ # @return [void]
87
+ def apply_bff_configuration(target, options)
88
+ if options.respond_to?(:call)
89
+ target.configure(&options)
90
+ return
91
+ end
92
+
93
+ hash = options.respond_to?(:to_h) ? options.to_h : options
94
+ return unless hash.respond_to?(:each)
95
+
96
+ entries = hash.transform_keys(&:to_sym)
97
+
98
+ return if entries.empty?
99
+
100
+ target.configure do |config|
101
+ entries.each do |key, value|
102
+ writer = "#{key}="
103
+ config.public_send(writer, value) if config.respond_to?(writer)
104
+ end
105
+ end
106
+ end
107
+
68
108
  # Sync configuration from the Rails application into Verikloak::Rails.
69
109
  #
70
110
  # @param app [Rails::Application]
@@ -72,17 +112,33 @@ module Verikloak
72
112
  def apply_configuration(app)
73
113
  Verikloak::Rails.configure do |c|
74
114
  rails_cfg = app.config.verikloak
75
- %i[discovery_url audience issuer leeway skip_paths
76
- logger_tags error_renderer auto_include_controller
77
- render_500_json rescue_pundit middleware_insert_before
78
- middleware_insert_after auto_insert_bff_header_guard
79
- bff_header_guard_insert_before bff_header_guard_insert_after].each do |key|
115
+ CONFIG_KEYS.each do |key|
80
116
  c.send("#{key}=", rails_cfg[key]) if rails_cfg.key?(key)
81
117
  end
82
118
  c.rescue_pundit = false if !rails_cfg.key?(:rescue_pundit) && defined?(::Verikloak::Pundit)
83
119
  end
84
120
  end
85
121
 
122
+ # Configure the verikloak-bff library when options are supplied.
123
+ #
124
+ # @return [void]
125
+ def configure_bff_library
126
+ options = Verikloak::Rails.config.bff_header_guard_options
127
+ return if options.nil? || (options.respond_to?(:empty?) && options.empty?)
128
+
129
+ target = if defined?(::Verikloak::BFF) && ::Verikloak::BFF.respond_to?(:configure)
130
+ ::Verikloak::BFF
131
+ elsif defined?(::Verikloak::Bff) && ::Verikloak::Bff.respond_to?(:configure)
132
+ ::Verikloak::Bff
133
+ end
134
+
135
+ return unless target
136
+
137
+ apply_bff_configuration(target, options)
138
+ rescue StandardError => e
139
+ warn_with_fallback("[verikloak] Failed to apply BFF configuration: #{e.message}")
140
+ end
141
+
86
142
  # Check if discovery_url is present and valid.
87
143
  #
88
144
  # @return [Boolean] true if discovery_url is configured and not empty
@@ -102,11 +158,7 @@ module Verikloak
102
158
  # @return [void]
103
159
  def log_missing_discovery_url_warning
104
160
  message = '[verikloak] discovery_url is not configured; skipping middleware insertion.'
105
- if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
106
- ::Rails.logger.warn(message)
107
- else
108
- warn(message)
109
- end
161
+ warn_with_fallback(message)
110
162
  end
111
163
 
112
164
  # Insert the base Verikloak::Middleware into the application middleware stack.
@@ -136,30 +188,36 @@ module Verikloak
136
188
  # @param base_options [Hash] options to pass to the middleware
137
189
  # @return [void]
138
190
  def insert_middleware_after(stack, base_options)
139
- candidates = middleware_insert_after_candidates
140
- inserted = false
141
-
142
- candidates.each do |candidate|
143
- next unless candidate
144
-
145
- begin
146
- stack.insert_after candidate,
147
- ::Verikloak::Middleware,
148
- **base_options
149
- inserted = true
150
- break
151
- rescue StandardError => e
152
- # Handle middleware insertion failures:
153
- # - Rails 8+: RuntimeError for missing middleware
154
- # - Earlier versions: ActionDispatch::MiddlewareStack::MiddlewareNotFound
155
- log_middleware_insertion_warning(candidate, e)
156
- end
191
+ inserted = middleware_insert_after_candidates.any? do |candidate|
192
+ try_insert_after(stack, candidate, base_options)
157
193
  end
158
194
 
159
195
  # Only use as fallback if insertion after a specific middleware failed
160
196
  stack.use ::Verikloak::Middleware, **base_options unless inserted
161
197
  end
162
198
 
199
+ # Attempt to insert after a candidate middleware.
200
+ # Logs a warning and returns false when the candidate is not present.
201
+ #
202
+ # @param stack [ActionDispatch::MiddlewareStackProxy]
203
+ # @param candidate [Object, nil]
204
+ # @param base_options [Hash]
205
+ # @return [Boolean]
206
+ def try_insert_after(stack, candidate, base_options)
207
+ return false unless candidate
208
+
209
+ stack.insert_after candidate,
210
+ ::Verikloak::Middleware,
211
+ **base_options
212
+ true
213
+ rescue StandardError => e
214
+ # Handle middleware insertion failures:
215
+ # - Rails 8+: RuntimeError for missing middleware
216
+ # - Earlier versions: ActionDispatch::MiddlewareStack::MiddlewareNotFound
217
+ log_middleware_insertion_warning(candidate, e)
218
+ false
219
+ end
220
+
163
221
  # Build list of middleware to try as insertion points.
164
222
  # Starts with the configured value (if any) and falls back to defaults
165
223
  # that exist across supported Rails versions.
@@ -185,8 +243,23 @@ module Verikloak
185
243
  def log_middleware_insertion_warning(candidate, error)
186
244
  candidate_name = candidate.is_a?(Class) ? candidate.name : candidate.class.name
187
245
  message = "[verikloak] Unable to insert after #{candidate_name}: #{error.message}"
188
- if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
189
- ::Rails.logger.warn(message)
246
+ warn_with_fallback(message)
247
+ end
248
+
249
+ # Resolve the logger instance used for warnings, if present.
250
+ # @return [Object, nil]
251
+ def rails_logger
252
+ return unless defined?(::Rails) && ::Rails.respond_to?(:logger)
253
+
254
+ ::Rails.logger
255
+ end
256
+
257
+ # Log a warning using Rails.logger when available, otherwise fall back to Kernel#warn.
258
+ # @param message [String]
259
+ # @return [void]
260
+ def warn_with_fallback(message)
261
+ if (logger = rails_logger)
262
+ logger.warn(message)
190
263
  else
191
264
  warn(message)
192
265
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Verikloak
4
4
  module Rails
5
- VERSION = '0.2.7'
5
+ VERSION = '0.2.8'
6
6
  end
7
7
  end
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: 0.2.7
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -94,7 +94,7 @@ metadata:
94
94
  source_code_uri: https://github.com/taiyaky/verikloak-rails
95
95
  changelog_uri: https://github.com/taiyaky/verikloak-rails/blob/main/CHANGELOG.md
96
96
  bug_tracker_uri: https://github.com/taiyaky/verikloak-rails/issues
97
- documentation_uri: https://rubydoc.info/gems/verikloak-rails/0.2.7
97
+ documentation_uri: https://rubydoc.info/gems/verikloak-rails/0.2.8
98
98
  rubygems_mfa_required: 'true'
99
99
  rdoc_options: []
100
100
  require_paths: