verikloak-bff 0.2.6 → 0.3.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: edf579dc8e103482358c6ef7577c90c0b4aa24ae53b31c0c82bf89e5ffb4a68a
4
- data.tar.gz: b976af45f7ffef721bd69263ed29cafaea1fdd711a1f94fff75a0045af8fc648
3
+ metadata.gz: 8a79baf28ee05a8774b36916c71281bd72cd35825148413c15e9f865b85416cb
4
+ data.tar.gz: 739244544590c0f306eff4d587c27c312adc4c081646a45d2ec4a58e83ce6abb
5
5
  SHA512:
6
- metadata.gz: 2354503e6faf4768d680057ae347e44d5e0eadfe29ef49f01585a0db859addcb53f881e49f73956e07721799534ac444c2ae13f6c9ac93983256d4d61bdaad0d
7
- data.tar.gz: 6543a6dd768d1cd2d11fe71a976dcbbd52bd1ad40021177a7b798dfd4b20b9560384b13cc1f1adb861b61865f5a076b746999d7939b225b65a54bda2249106dd
6
+ metadata.gz: 713b4e27ee284e38917a6fd4880414cdb8792fbffd4f7713112d2fa619f5ecf3120379a4a0b29eb115ce075c6373e0a4b25f1bca38bd8e8af0ff683944dcd8fd
7
+ data.tar.gz: ed258207b16fdb79fb079656393e0f7fd4bc1a650d942c6c19060c0aeaf45f8e63fa393a115bb8f80ea87f9c0d84cd025bb55a5ffb5259a981007211c896b786
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.0] - 2025-01-01
11
+
12
+ ### Added
13
+ - **`disabled` configuration option**: Explicitly disable the middleware in pass-through mode. When `disabled: false` (default) and `trusted_proxies` is not configured, a `ConfigurationError` is raised at startup.
14
+
15
+ ### Changed
16
+ - **BREAKING**: `trusted_proxies` is now **required** when `disabled: false`. Previously, an empty `trusted_proxies` would silently disable the middleware (fail-open). Now it raises `Verikloak::BFF::HeaderGuard::ConfigurationError` to prevent unintended security gaps.
17
+
18
+ ### Fixed
19
+ - **Security**: Prevent fail-open vulnerability where unset `trusted_proxies` could silently bypass proxy trust validation.
20
+
21
+ ---
22
+
10
23
  ## [0.2.6] - 2025-12-31
11
24
 
12
25
  ### Fixed
data/README.md CHANGED
@@ -42,7 +42,7 @@ Run the install generator to create a configuration file:
42
42
  bin/rails g verikloak:bff:install
43
43
  ```
44
44
 
45
- The generator creates `config/initializers/verikloak_bff.rb` with BFF configuration options. **Edit this file to set `trusted_proxies`** (required) and customize other options as needed.
45
+ The generator creates `config/initializers/verikloak_bff.rb` with BFF configuration options. **Edit this file to set `trusted_proxies`** (required unless `disabled: true`) and customize other options as needed.
46
46
 
47
47
  **Note**: Middleware insertion is handled automatically by `verikloak-rails`. The generated initializer only contains configuration—no manual middleware setup is required.
48
48
 
@@ -69,7 +69,8 @@ See `examples/rack.ru` for a tiny Rack app demo.
69
69
 
70
70
  | Key | Type | Default | Description |
71
71
  |----------------------------- |--------------------------------------|--------------|-------------|
72
- | `trusted_proxies` | Array[String/Regexp/Proc] | *(required)* | Allowlist for proxy peers (by IP/CIDR/regex/proc). |
72
+ | `disabled` | Boolean | `false` | Explicitly disable the middleware (pass-through mode). When `false`, `trusted_proxies` must be configured. |
73
+ | `trusted_proxies` | Array[String/Regexp/Proc] | *(required)* | Allowlist for proxy peers (by IP/CIDR/regex/proc). **Required** unless `disabled: true`. |
73
74
  | `prefer_forwarded` | Boolean | `true` | Prefer `X-Forwarded-Access-Token` over `Authorization`. |
74
75
  | `require_forwarded_header` | Boolean | `false` | Reject when no `X-Forwarded-Access-Token` (blocks direct access). |
75
76
  | `enforce_header_consistency` | Boolean | `true` | If both headers exist, require identical token values. |
@@ -84,6 +85,19 @@ See `examples/rack.ru` for a tiny Rack app demo.
84
85
  | `forwarded_header_name` | String | `HTTP_X_FORWARDED_ACCESS_TOKEN` | Env key for forwarded access token. |
85
86
  | `auth_request_headers` | Hash | see code | Mapping for `X-Auth-Request-*` env keys: `{ email, user, groups }`. |
86
87
 
88
+ ### Configuration Requirements
89
+
90
+ When `disabled: false` (the default), you **must** configure `trusted_proxies`. If it is not set, the middleware will raise a `ConfigurationError` at startup to prevent a fail-open security vulnerability.
91
+
92
+ To explicitly disable the middleware (e.g., for development or testing), set `disabled: true`:
93
+
94
+ ```ruby
95
+ # Pass-through mode: no proxy trust checking
96
+ Verikloak::BFF.configure do |config|
97
+ config.disabled = true
98
+ end
99
+ ```
100
+
87
101
  ## Errors
88
102
  This gem returns concise, RFC 6750–style error responses with stable codes. See [ERRORS.md](ERRORS.md) for details and examples.
89
103
 
@@ -20,7 +20,18 @@
20
20
  #
21
21
  Verikloak::BFF.configure do |config|
22
22
  # ==========================================================================
23
- # Trust Boundary (REQUIRED)
23
+ # Disable Middleware (Development/Testing Only)
24
+ # ==========================================================================
25
+ # Explicitly disable the middleware (pass-through mode).
26
+ # When disabled: false (default), trusted_proxies MUST be configured.
27
+ #
28
+ # SECURITY WARNING: Only set to true in development/test environments.
29
+ # Production deployments should ALWAYS configure trusted_proxies.
30
+ #
31
+ # config.disabled = ENV.fetch('VERIKLOAK_BFF_DISABLED', 'false') == 'true'
32
+
33
+ # ==========================================================================
34
+ # Trust Boundary (REQUIRED when disabled: false)
24
35
  # ==========================================================================
25
36
  # Allowlist for trusted proxy peers. Requests from untrusted peers are rejected.
26
37
  # Supports IP addresses, CIDR ranges, Regexp, or Proc.
@@ -28,6 +39,9 @@ Verikloak::BFF.configure do |config|
28
39
  # SECURITY: Keep this list as specific as possible. Review whenever proxy
29
40
  # topology changes to avoid unintentionally widening the trust boundary.
30
41
  #
42
+ # NOTE: This setting is REQUIRED. If not configured and disabled is false,
43
+ # a ConfigurationError will be raised at startup to prevent fail-open vulnerabilities.
44
+ #
31
45
  config.trusted_proxies = ENV.fetch('TRUSTED_PROXIES', '127.0.0.1').split(',').map(&:strip)
32
46
 
33
47
  # ==========================================================================
@@ -3,6 +3,8 @@
3
3
  # Configuration object for verikloak-bff. Holds middleware settings such as
4
4
  # proxy trust rules, header consistency policies, and logging options.
5
5
  #
6
+ # @!attribute [rw] disabled
7
+ # @return [Boolean] explicitly disable the middleware (pass-through mode)
6
8
  # @!attribute [rw] trusted_proxies
7
9
  # @return [Array<String, Regexp, Proc>] allowlist of trusted proxy peers
8
10
  # @!attribute [rw] prefer_forwarded
@@ -28,7 +30,7 @@ module Verikloak
28
30
  module BFF
29
31
  # Configuration for Verikloak::BFF middleware (trusted proxies, header policies, logging, etc.).
30
32
  class Configuration
31
- attr_accessor :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
33
+ attr_accessor :disabled, :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
32
34
  :enforce_header_consistency, :enforce_claims_consistency,
33
35
  :strip_suspicious_headers, :xff_strategy, :clock_skew_leeway,
34
36
  :logger, :peer_preference, :auth_request_headers, :log_with,
@@ -43,6 +45,7 @@ module Verikloak
43
45
  #
44
46
  # @return [void]
45
47
  def initialize
48
+ @disabled = false
46
49
  @trusted_proxies = []
47
50
  @prefer_forwarded = true
48
51
  @require_forwarded_header = false
@@ -96,10 +96,78 @@ module Verikloak
96
96
  def sanitize_string(value)
97
97
  value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').gsub(LOG_CONTROL_CHARS, '')
98
98
  end
99
+
100
+ # Describe the source of the chosen token for logging purposes.
101
+ #
102
+ # @param prefer_forwarded [Boolean]
103
+ # @param auth_token [String, nil]
104
+ # @param fwd_token [String, nil]
105
+ # @return [String] "authorization" or "forwarded"
106
+ def token_source(prefer_forwarded, auth_token, fwd_token)
107
+ return 'forwarded' if prefer_forwarded && fwd_token
108
+
109
+ if auth_token && fwd_token
110
+ 'authorization'
111
+ else
112
+ (auth_token ? 'authorization' : 'forwarded')
113
+ end
114
+ end
115
+
116
+ # Extract request id for logging from common headers.
117
+ #
118
+ # @param env [Hash]
119
+ # @return [String, nil]
120
+ def request_id(env)
121
+ env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
122
+ end
123
+
124
+ # Emit a structured log line if a logger is present.
125
+ #
126
+ # @param env [Hash]
127
+ # @param config [Configuration]
128
+ # @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
129
+ # @param attrs [Hash]
130
+ # @return [void]
131
+ def log_event(env, config, kind, **attrs)
132
+ lg = config.logger || env['rack.logger']
133
+ payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
134
+ sanitized = sanitize_payload(payload)
135
+ if config.log_with.respond_to?(:call)
136
+ begin
137
+ config.log_with.call(sanitized)
138
+ rescue StandardError
139
+ # ignore log hook failures
140
+ end
141
+ end
142
+ return unless lg
143
+
144
+ msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
145
+ level = (kind == :ok ? :info : :warn)
146
+ lg.public_send(level, msg)
147
+ rescue StandardError
148
+ # no-op on logging errors
149
+ end
150
+
151
+ # Build a minimal RFC6750-style error response.
152
+ #
153
+ # @param env [Hash]
154
+ # @param config [Configuration]
155
+ # @param error [Verikloak::BFF::Error]
156
+ # @return [Array(Integer, Hash, Array<String>)]
157
+ def respond_with_error(env, config, error)
158
+ log_event(env, config, :error, code: error.code)
159
+ body = { error: error.code, message: error.message }.to_json
160
+ headers = { 'Content-Type' => 'application/json',
161
+ 'WWW-Authenticate' => %(Bearer error="#{error.code}", error_description="#{error.message}") }
162
+ [error.http_status, headers, [body]]
163
+ end
99
164
  end
100
165
 
101
166
  # Rack middleware that enforces BFF boundary and header/claims consistency.
102
167
  class HeaderGuard
168
+ # Error raised when trusted_proxies is not configured and disabled is not explicitly set.
169
+ class ConfigurationError < StandardError; end
170
+
103
171
  RequestTokens = Struct.new(:auth, :forwarded, :chosen)
104
172
 
105
173
  # Accept both Rack 2 and Rack 3 builder call styles:
@@ -109,6 +177,7 @@ module Verikloak
109
177
  # @param app [#call]
110
178
  # @param opts [Hash, nil]
111
179
  # @param opts_kw [Hash]
180
+ # @raise [ConfigurationError] when trusted_proxies is not configured and disabled is false
112
181
  def initialize(app, opts = nil, **opts_kw)
113
182
  @app = app
114
183
  # Use a per-instance copy of global config to avoid cross-request/test side effects
@@ -118,9 +187,15 @@ module Verikloak
118
187
  combined.merge!(opts_kw) if opts_kw && !opts_kw.empty?
119
188
  apply_overrides!(combined)
120
189
 
121
- return unless @config.trusted_proxies.nil? || @config.trusted_proxies.empty?
190
+ # Check configuration validity
191
+ validate_configuration!
192
+ end
122
193
 
123
- raise ArgumentError, 'trusted_proxies must be configured'
194
+ # Check if the middleware is explicitly disabled.
195
+ #
196
+ # @return [Boolean]
197
+ def disabled?
198
+ @config.disabled
124
199
  end
125
200
 
126
201
  # Process a Rack request through the BFF header guard pipeline.
@@ -131,9 +206,14 @@ module Verikloak
131
206
  # 3. Policy enforcement (forwarded token requirements, consistency checks)
132
207
  # 4. Request finalization (Authorization header normalization)
133
208
  #
209
+ # When `disabled: true` is set, the middleware passes through without any processing.
210
+ #
134
211
  # @param env [Hash]
135
212
  # @return [Array(Integer, Hash, Array<#to_s>)] Rack response triple
136
213
  def call(env)
214
+ # Pass through if middleware is explicitly disabled
215
+ return @app.call(env) if disabled?
216
+
137
217
  # Stage 1: Validate request comes from trusted proxy
138
218
  ensure_trusted_proxy!(env)
139
219
 
@@ -148,11 +228,28 @@ module Verikloak
148
228
 
149
229
  @app.call(env)
150
230
  rescue Verikloak::BFF::Error => e
151
- respond_with_error(env, e)
231
+ HeaderGuardSanitizer.respond_with_error(env, @config, e)
152
232
  end
153
233
 
154
234
  private
155
235
 
236
+ # Validate that required configuration is present.
237
+ # Raises ConfigurationError if trusted_proxies is not configured and disabled is false.
238
+ #
239
+ # @raise [ConfigurationError]
240
+ # @return [void]
241
+ def validate_configuration!
242
+ return if @config.disabled
243
+
244
+ proxies = @config.trusted_proxies
245
+ return unless proxies.nil? || proxies.empty?
246
+
247
+ raise ConfigurationError,
248
+ 'trusted_proxies must be configured for Verikloak::BFF::HeaderGuard. ' \
249
+ 'Set trusted_proxies to an array of allowed proxy addresses/CIDRs, ' \
250
+ 'or set disabled: true to explicitly skip BFF protection.'
251
+ end
252
+
156
253
  # Build token state by extracting, validating, and selecting the active token.
157
254
  #
158
255
  # @param env [Hash]
@@ -210,63 +307,6 @@ module Verikloak
210
307
  auth_token || fwd_token
211
308
  end
212
309
 
213
- # Describe the source of the chosen token for logging purposes.
214
- #
215
- # @param auth_token [String, nil]
216
- # @param fwd_token [String, nil]
217
- # @return [String] "authorization" or "forwarded"
218
- def token_source(auth_token, fwd_token)
219
- return 'forwarded' if @config.prefer_forwarded && fwd_token
220
-
221
- if auth_token && fwd_token
222
- 'authorization'
223
- else
224
- (auth_token ? 'authorization' : 'forwarded')
225
- end
226
- end
227
-
228
- # Extract request id for logging from common headers.
229
- #
230
- # @param env [Hash]
231
- # @return [String, nil]
232
- def request_id(env)
233
- env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
234
- end
235
-
236
- # Resolve logger to use (config-provided or rack.logger).
237
- #
238
- # @param env [Hash]
239
- # @return [Logger, nil]
240
- def logger(env)
241
- @config.logger || env['rack.logger']
242
- end
243
-
244
- # Emit a structured log line if a logger is present.
245
- #
246
- # @param env [Hash]
247
- # @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
248
- # @param attrs [Hash]
249
- # @return [void]
250
- def log_event(env, kind, **attrs)
251
- lg = logger(env)
252
- payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
253
- sanitized = HeaderGuardSanitizer.sanitize_payload(payload)
254
- if @config.log_with.respond_to?(:call)
255
- begin
256
- @config.log_with.call(sanitized)
257
- rescue StandardError
258
- # ignore log hook failures
259
- end
260
- end
261
- return unless lg
262
-
263
- msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
264
- level = (kind == :ok ? :info : :warn)
265
- lg.public_send(level, msg)
266
- rescue StandardError
267
- # no-op on logging errors
268
- end
269
-
270
310
  # Raise when the request did not come through a trusted proxy.
271
311
  #
272
312
  # @param env [Hash]
@@ -300,7 +340,7 @@ module Verikloak
300
340
  digest_b = ::Digest::SHA256.hexdigest(fwd_token)
301
341
  return if Rack::Utils.secure_compare(digest_a, digest_b)
302
342
 
303
- log_event(env, :mismatch, reason: 'authorization_vs_forwarded')
343
+ HeaderGuardSanitizer.log_event(env, @config, :mismatch, reason: 'authorization_vs_forwarded')
304
344
  raise HeaderMismatchError
305
345
  end
306
346
 
@@ -315,7 +355,7 @@ module Verikloak
315
355
  return unless res.is_a?(Array) && res.first == :error
316
356
 
317
357
  field = res.last
318
- log_event(env, :claims_mismatch, field: field.to_s)
358
+ HeaderGuardSanitizer.log_event(env, @config, :claims_mismatch, field: field.to_s)
319
359
  return if claims_consistency_log_only?
320
360
 
321
361
  raise ClaimsMismatchError, field
@@ -342,20 +382,8 @@ module Verikloak
342
382
  return unless chosen
343
383
 
344
384
  ForwardedToken.set_authorization!(env, chosen)
345
- log_event(env, :ok, source: token_source(auth_token, fwd_token), **HeaderGuardSanitizer.token_tags(chosen))
346
- end
347
-
348
- # Build a minimal RFC6750-style error response.
349
- #
350
- # @param env [Hash]
351
- # @param error [Verikloak::BFF::Error]
352
- # @return [Array(Integer, Hash, Array<String>)]
353
- def respond_with_error(env, error)
354
- log_event(env, :error, code: error.code)
355
- body = { error: error.code, message: error.message }.to_json
356
- headers = { 'Content-Type' => 'application/json',
357
- 'WWW-Authenticate' => %(Bearer error="#{error.code}", error_description="#{error.message}") }
358
- [error.http_status, headers, [body]]
385
+ source = HeaderGuardSanitizer.token_source(@config.prefer_forwarded, auth_token, fwd_token)
386
+ HeaderGuardSanitizer.log_event(env, @config, :ok, source: source, **HeaderGuardSanitizer.token_tags(chosen))
359
387
  end
360
388
 
361
389
  # Resolve the first env header from which to source a bearer token.
@@ -5,6 +5,6 @@
5
5
  # @return [String]
6
6
  module Verikloak
7
7
  module BFF
8
- VERSION = '0.2.6'
8
+ VERSION = '0.3.0'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verikloak-bff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -101,7 +101,7 @@ metadata:
101
101
  source_code_uri: https://github.com/taiyaky/verikloak-bff
102
102
  changelog_uri: https://github.com/taiyaky/verikloak-bff/blob/main/CHANGELOG.md
103
103
  bug_tracker_uri: https://github.com/taiyaky/verikloak-bff/issues
104
- documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.2.6
104
+ documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.3.0
105
105
  rubygems_mfa_required: 'true'
106
106
  rdoc_options: []
107
107
  require_paths: