verikloak-bff 0.2.5 → 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: b024a1e87d9a4cd895b0e052ba276cbf418b50d4f51b3f642d3916e7b713253e
4
- data.tar.gz: 82bab8741e03c8b1f57bea89856b8811440c34778470c18a82c87c279a36c4a4
3
+ metadata.gz: 8a79baf28ee05a8774b36916c71281bd72cd35825148413c15e9f865b85416cb
4
+ data.tar.gz: 739244544590c0f306eff4d587c27c312adc4c081646a45d2ec4a58e83ce6abb
5
5
  SHA512:
6
- metadata.gz: c2e1f2587d253acd2ac1f3a26b33d701adbe7cce48a6646f95658bd503e5238033bd855ab39fe23dc8ebb85af5bbb449b6170acba6e5bb840e8878afcd9e9d79
7
- data.tar.gz: ecf08756e3af68c6d199ff54e40f10aee69a0a5c62d49cde1c5b1bd732dc319510490131e9a0ea357c82b67af7710ba7bd4a7303549b842b7a78fcddba86ad49
6
+ metadata.gz: 713b4e27ee284e38917a6fd4880414cdb8792fbffd4f7713112d2fa619f5ecf3120379a4a0b29eb115ce075c6373e0a4b25f1bca38bd8e8af0ff683944dcd8fd
7
+ data.tar.gz: ed258207b16fdb79fb079656393e0f7fd4bc1a650d942c6c19060c0aeaf45f8e63fa393a115bb8f80ea87f9c0d84cd025bb55a5ffb5259a981007211c896b786
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ 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
+
23
+ ## [0.2.6] - 2025-12-31
24
+
25
+ ### Fixed
26
+ - **Rails 8.x+ compatibility**: Remove `after_initialize` middleware insertion from generator template to avoid `FrozenError` when middleware stack is frozen.
27
+
28
+ ### Changed
29
+ - Generator (`rails g verikloak:bff:install`) now creates a **configuration-only** initializer. Middleware insertion is handled automatically by `verikloak-rails`.
30
+ - Generated initializer includes comprehensive configuration options with documentation comments.
31
+ - **Breaking**: Minimum `verikloak` dependency raised from `>= 0.2.0` to `>= 0.3.0`.
32
+
33
+ ### Documentation
34
+ - Add "Rails Integration" section explaining automatic middleware detection with `verikloak-rails`.
35
+ - Add warning about Rails 8.x+ middleware stack freeze in `after_initialize`.
36
+ - Add "oauth2-proxy Integration" section with header configuration reference and recommended settings.
37
+ - Document manual middleware setup option for users not using `verikloak-rails`.
38
+ - Update `docs/rails.md` with clearer setup instructions and Rails 8.x support note.
39
+
10
40
  ## [0.2.5] - 2025-09-28
11
41
 
12
42
  ### Changed
data/README.md CHANGED
@@ -31,16 +31,25 @@ use Verikloak::BFF::HeaderGuard, trusted_proxies: ['127.0.0.1', '10.0.0.0/8']
31
31
  ```
32
32
 
33
33
  ### Rails Applications
34
- Add the gem to your Gemfile and run the install generator to drop an initializer that wires the middleware into Rails:
34
+ Add both gems to your Gemfile:
35
35
  ```ruby
36
+ gem 'verikloak-rails'
36
37
  gem 'verikloak-bff'
37
38
  ```
38
39
 
40
+ Run the install generator to create a configuration file:
39
41
  ```sh
40
42
  bin/rails g verikloak:bff:install
41
43
  ```
42
44
 
43
- The generated initializer inserts `Verikloak::BFF::HeaderGuard` after the core `Verikloak::Middleware` during boot. If the core middleware is not present (for example, when verikloak-rails has not been fully configured yet), the initializer logs a warning and allows Rails to boot normally.
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
+
47
+ **Note**: Middleware insertion is handled automatically by `verikloak-rails`. The generated initializer only contains configuration—no manual middleware setup is required.
48
+
49
+ > **Without verikloak-rails**: You can also configure middleware manually in `config/application.rb`:
50
+ > ```ruby
51
+ > config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
52
+ > ```
44
53
 
45
54
  For detailed configuration, proxy setup examples, and troubleshooting, see [docs/rails.md](docs/rails.md).
46
55
 
@@ -60,7 +69,8 @@ See `examples/rack.ru` for a tiny Rack app demo.
60
69
 
61
70
  | Key | Type | Default | Description |
62
71
  |----------------------------- |--------------------------------------|--------------|-------------|
63
- | `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`. |
64
74
  | `prefer_forwarded` | Boolean | `true` | Prefer `X-Forwarded-Access-Token` over `Authorization`. |
65
75
  | `require_forwarded_header` | Boolean | `false` | Reject when no `X-Forwarded-Access-Token` (blocks direct access). |
66
76
  | `enforce_header_consistency` | Boolean | `true` | If both headers exist, require identical token values. |
@@ -75,6 +85,19 @@ See `examples/rack.ru` for a tiny Rack app demo.
75
85
  | `forwarded_header_name` | String | `HTTP_X_FORWARDED_ACCESS_TOKEN` | Env key for forwarded access token. |
76
86
  | `auth_request_headers` | Hash | see code | Mapping for `X-Auth-Request-*` env keys: `{ email, user, groups }`. |
77
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
+
78
101
  ## Errors
79
102
  This gem returns concise, RFC 6750–style error responses with stable codes. See [ERRORS.md](ERRORS.md) for details and examples.
80
103
 
@@ -110,6 +133,95 @@ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/r
110
133
  - Claims consistency modes
111
134
  - Default `:enforce` mode rejects requests with mismatches. Switch to `claims_consistency_mode: :log_only` when you only need observability signals; downstream services must continue verifying JWT signatures, issuer, audience, and expirations.
112
135
 
136
+ ## Rails Integration
137
+
138
+ ### Recommended: Use with verikloak-rails (Auto-detection)
139
+
140
+ When both `verikloak-rails` and `verikloak-bff` are installed, the railtie automatically inserts the BFF middleware. No manual configuration is required.
141
+
142
+ ```ruby
143
+ # Gemfile
144
+ gem 'verikloak-rails'
145
+ gem 'verikloak-bff'
146
+ ```
147
+
148
+ The middleware stack will be configured as:
149
+ ```
150
+ [Verikloak::BFF::HeaderGuard] → [Verikloak::Middleware] → [Your App]
151
+ ```
152
+
153
+ ### ⚠️ Rails 8.x+ Middleware Stack Freeze
154
+
155
+ In Rails 8.x and later, the middleware stack is frozen after the `after_initialize` callback. Manual middleware insertion in `after_initialize` will raise `FrozenError`:
156
+
157
+ ```ruby
158
+ # ❌ This will NOT work in Rails 8.x+
159
+ Rails.application.config.after_initialize do
160
+ Rails.application.config.middleware.insert_after(
161
+ Verikloak::Middleware,
162
+ Verikloak::BFF::HeaderGuard
163
+ )
164
+ end
165
+ # => FrozenError: can't modify frozen Array
166
+ ```
167
+
168
+ **Solution**: Use the automatic detection feature with `verikloak-rails`, or insert middleware in `config/application.rb` or an initializer (which runs before the stack is frozen):
169
+
170
+ ```ruby
171
+ # ✅ This works - config/application.rb
172
+ module MyApp
173
+ class Application < Rails::Application
174
+ config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
175
+ end
176
+ end
177
+ ```
178
+
179
+ ## oauth2-proxy Integration
180
+
181
+ ### Header Configuration Reference
182
+
183
+ | oauth2-proxy Setting | Header Sent | HeaderGuard Behavior |
184
+ |---------------------|-------------|---------------------|
185
+ | `--pass-access-token=true` | Cookie (`_oauth2_proxy`) | Not supported (cookie-based) |
186
+ | `--pass-access-token=true` + nginx | `X-Forwarded-Access-Token` | Normalized to `Authorization` (default) |
187
+ | `--set-authorization-header=true` | `Authorization: Bearer <token>` | Used directly |
188
+ | `--set-xauthrequest=true` | `X-Auth-Request-Access-Token` | Requires `token_header_priority` config |
189
+
190
+ ### Recommended oauth2-proxy Configuration
191
+
192
+ For best compatibility, configure oauth2-proxy to emit `X-Forwarded-Access-Token` via nginx:
193
+
194
+ ```bash
195
+ oauth2-proxy \
196
+ --pass-access-token=true \
197
+ --set-authorization-header=false \
198
+ --set-xauthrequest=true \
199
+ --upstream=http://rails-api:3000
200
+ ```
201
+
202
+ Then configure nginx to relay the token:
203
+
204
+ ```nginx
205
+ proxy_set_header X-Forwarded-Access-Token $upstream_http_x_auth_request_access_token;
206
+ ```
207
+
208
+ ### Using X-Auth-Request-Access-Token Directly
209
+
210
+ If you prefer to use `X-Auth-Request-Access-Token` without nginx relay, configure both `forwarded_header_name` and `token_header_priority`:
211
+
212
+ ```ruby
213
+ use Verikloak::BFF::HeaderGuard,
214
+ trusted_proxies: ['10.0.0.0/8'],
215
+ # Set the forwarded header to X-Auth-Request-Access-Token
216
+ forwarded_header_name: 'HTTP_X_AUTH_REQUEST_ACCESS_TOKEN',
217
+ # Also add to priority list for Authorization seeding
218
+ token_header_priority: ['HTTP_X_AUTH_REQUEST_ACCESS_TOKEN']
219
+ ```
220
+
221
+ > **Note**: If you only set `token_header_priority` without changing `forwarded_header_name`,
222
+ > `require_forwarded_header: true` will still expect `X-Forwarded-Access-Token` and return 401
223
+ > when it's missing.
224
+
113
225
  ## Development (for contributors)
114
226
  Clone and install dependencies:
115
227
 
@@ -1,11 +1,129 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Configure verikloak-bff to insert its HeaderGuard middleware only after the
4
- # Verikloak core middleware is present. This avoids boot errors when the core
5
- # gem has not yet been installed.
6
- Rails.application.config.after_initialize do
7
- Verikloak::BFF::Rails::Middleware.insert_after_core(
8
- Rails.application.config.middleware,
9
- logger: Rails.logger
10
- )
3
+ # Verikloak BFF Configuration
4
+ #
5
+ # This file configures the Verikloak::BFF::HeaderGuard middleware.
6
+ #
7
+ # IMPORTANT: Middleware insertion is handled automatically by verikloak-rails.
8
+ # Make sure you have both gems in your Gemfile:
9
+ #
10
+ # gem 'verikloak-rails'
11
+ # gem 'verikloak-bff'
12
+ #
13
+ # The middleware stack will be configured as:
14
+ # [Verikloak::BFF::HeaderGuard] → [Verikloak::Middleware] → [Your App]
15
+ #
16
+ # If you are NOT using verikloak-rails, you must manually insert the middleware
17
+ # in config/application.rb (NOT in after_initialize, which causes FrozenError in Rails 8.x+):
18
+ #
19
+ # config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
20
+ #
21
+ Verikloak::BFF.configure do |config|
22
+ # ==========================================================================
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)
35
+ # ==========================================================================
36
+ # Allowlist for trusted proxy peers. Requests from untrusted peers are rejected.
37
+ # Supports IP addresses, CIDR ranges, Regexp, or Proc.
38
+ #
39
+ # SECURITY: Keep this list as specific as possible. Review whenever proxy
40
+ # topology changes to avoid unintentionally widening the trust boundary.
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
+ #
45
+ config.trusted_proxies = ENV.fetch('TRUSTED_PROXIES', '127.0.0.1').split(',').map(&:strip)
46
+
47
+ # ==========================================================================
48
+ # Peer Selection
49
+ # ==========================================================================
50
+ # How to determine the client's peer IP for trust decisions.
51
+ #
52
+ # :remote_then_xff (default) - Prefer REMOTE_ADDR, then fall back to XFF
53
+ # :xff_only - Use only X-Forwarded-For header
54
+ #
55
+ # config.peer_preference = :remote_then_xff
56
+
57
+ # Which X-Forwarded-For entry to use when multiple proxies are involved.
58
+ #
59
+ # :rightmost (default) - Nearest proxy (recommended for most setups)
60
+ # :leftmost - Original client (use only if you trust all proxies)
61
+ #
62
+ # config.xff_strategy = :rightmost
63
+
64
+ # ==========================================================================
65
+ # Token Selection & Header Handling
66
+ # ==========================================================================
67
+ # Prefer X-Forwarded-Access-Token over Authorization header.
68
+ #
69
+ # config.prefer_forwarded = true
70
+
71
+ # Reject requests without X-Forwarded-Access-Token (blocks direct access).
72
+ # Enable this to ensure all requests go through the BFF proxy.
73
+ #
74
+ # config.require_forwarded_header = false
75
+
76
+ # Require Authorization and X-Forwarded-Access-Token to contain the same token
77
+ # when both are present.
78
+ #
79
+ # config.enforce_header_consistency = true
80
+
81
+ # Remove external X-Auth-Request-* headers before passing downstream.
82
+ # Prevents header injection attacks.
83
+ #
84
+ # config.strip_suspicious_headers = true
85
+
86
+ # ==========================================================================
87
+ # Claims Consistency (Optional)
88
+ # ==========================================================================
89
+ # Compare X-Auth-Request-* headers against JWT claims.
90
+ # Useful for detecting token substitution attacks.
91
+ #
92
+ # config.enforce_claims_consistency = {
93
+ # email: :email, # X-Auth-Request-Email == JWT email claim
94
+ # user: :sub, # X-Auth-Request-User == JWT sub claim
95
+ # groups: :realm_roles # X-Auth-Request-Groups ⊆ JWT realm_access.roles
96
+ # }
97
+
98
+ # :enforce (default) - Reject mismatches with 403
99
+ # :log_only - Log mismatches but allow request to proceed
100
+ #
101
+ # config.claims_consistency_mode = :enforce
102
+
103
+ # ==========================================================================
104
+ # Header Name Customization
105
+ # ==========================================================================
106
+ # Customize forwarded token header name (if your proxy uses a different header).
107
+ #
108
+ # config.forwarded_header_name = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
109
+
110
+ # Customize X-Auth-Request-* header names.
111
+ #
112
+ # config.auth_request_headers = {
113
+ # email: 'HTTP_X_AUTH_REQUEST_EMAIL',
114
+ # user: 'HTTP_X_AUTH_REQUEST_USER',
115
+ # groups: 'HTTP_X_AUTH_REQUEST_GROUPS'
116
+ # }
117
+
118
+ # Priority list for seeding Authorization header when empty.
119
+ #
120
+ # config.token_header_priority = ['HTTP_X_FORWARDED_ACCESS_TOKEN']
121
+
122
+ # ==========================================================================
123
+ # Logging
124
+ # ==========================================================================
125
+ # Structured logging hook for observability.
126
+ # Payload includes: rid (request_id), sub, kid, iss, aud
127
+ #
128
+ # config.log_with = ->(payload) { Rails.logger.info(payload.to_json) }
11
129
  end
@@ -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.5'
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.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -55,7 +55,7 @@ dependencies:
55
55
  requirements:
56
56
  - - ">="
57
57
  - !ruby/object:Gem::Version
58
- version: 0.2.0
58
+ version: 0.3.0
59
59
  - - "<"
60
60
  - !ruby/object:Gem::Version
61
61
  version: 1.0.0
@@ -65,7 +65,7 @@ dependencies:
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 0.2.0
68
+ version: 0.3.0
69
69
  - - "<"
70
70
  - !ruby/object:Gem::Version
71
71
  version: 1.0.0
@@ -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.5
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: