verikloak-bff 0.1.1 → 0.1.2

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: 19452cfc021fd5ffde923f55f294aafb99c4b80506047d0ef195233d019c5adb
4
- data.tar.gz: 3211ddb4779f6bbe1b079f6c83b018bb5b7b881bb267d253f94ffaaa20006711
3
+ metadata.gz: 6901b11146849ea25306d9f78b3cf7d8903dce4374efcf1322a7aed6a3d52872
4
+ data.tar.gz: 9a9cd3f59250aa57599de7e973009dedff8d075a395632eef1a19a889fb6257c
5
5
  SHA512:
6
- metadata.gz: '0890fcfe4ac2af3ce8bdbc34ee0753744a3a56459a90da3441247a7d36779b28db68ae7998b349968f5ecfd937121d3fad96e2f21a092662da2cbedeb61a358c'
7
- data.tar.gz: 7a4e7888b2f2caafc0bd1345760d3ef5e049f860cc9f36dba028c2a2afebaf8625719b6b0dbad9f7a21c01db957368b5d10d73c3533f19ce8fdc9acb4513e3f0
6
+ metadata.gz: eed7006bf01e9a51b4824d8e4fdd2a759210270fdf5fdb07617a21b5b6e40862232dc162f8bec8e65fdb89cea59443eee7a030c7fdd2e335446c182b2745936a
7
+ data.tar.gz: d89ca6f1f4032198704a0167d1c3cad7b8b2b1adc68948bb06b0abbf45c4f51157e2d00f3906ad070c4c59c56ed7ddb31bd8f8a58cd06884304b890889685b91
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.1.2] - 2025-09-21
11
+
12
+ ### Added
13
+ - Configuration option `claims_consistency_mode` supporting `:log_only` so deployments can record mismatches without rejecting requests.
14
+
15
+ ### Changed
16
+ - Sanitize log payload strings (including JWT tags) before invoking hooks or emitting to loggers to mitigate log forging attempts.
17
+
18
+ ### Documentation
19
+ - Document trusted proxy hygiene, sanitized logging hooks, and the new log-only mode in the README and Rails guide.
20
+
10
21
  ## [0.1.1] - 2025-09-15
11
22
 
12
23
  ### Changed
data/README.md CHANGED
@@ -47,6 +47,7 @@ See `examples/rack.ru` for a tiny Rack app demo.
47
47
  | `require_forwarded_header` | Boolean | `false` | Reject when no `X-Forwarded-Access-Token` (blocks direct access). |
48
48
  | `enforce_header_consistency` | Boolean | `true` | If both headers exist, require identical token values. |
49
49
  | `enforce_claims_consistency` | Hash | `{}` | Mapping of header→claim to compare (e.g., `{ email: :email, user: :sub, groups: :realm_roles }`). |
50
+ | `claims_consistency_mode` | Symbol (`:enforce`/`:log_only`) | `:enforce` | When `:log_only`, mismatches are logged but the request continues (still require downstream JWT verification). |
50
51
  | `strip_suspicious_headers` | Boolean | `true` | Remove external `X-Auth-Request-*` before passing downstream. |
51
52
  | `xff_strategy` | Symbol (`:rightmost`/`:leftmost`) | `:rightmost` | Which peer to pick from `X-Forwarded-For`. |
52
53
  | `peer_preference` | Symbol (`:remote_then_xff`/`:xff_only`) | `:remote_then_xff` | Whether to prefer `REMOTE_ADDR` before falling back to XFF. |
@@ -69,6 +70,9 @@ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/r
69
70
  - Set `peer_preference: :remote_then_xff` (default) to evaluate trust using the direct peer first, then fall back to the nearest (rightmost) `X-Forwarded-For` value.
70
71
  - If you run only behind a single, known proxy chain and want to rely solely on XFF ordering, use `peer_preference: :xff_only` and control position with `xff_strategy`.
71
72
 
73
+ - Trusted proxy hygiene
74
+ - Keep `trusted_proxies` as specific as possible (individual IPs, tight CIDR ranges, or regexes). Review the list whenever proxy topology changes to avoid unintentionally widening the trust boundary.
75
+
72
76
  - Header name customization
73
77
  - Forwarded-access-token header can be changed via:
74
78
  - `forwarded_header_name: 'HTTP_X_CUSTOM_FORWARD_TOKEN'`
@@ -81,9 +85,12 @@ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/r
81
85
 
82
86
  - Observability helpers
83
87
  - Downstream can inspect `env['verikloak.bff.token']` (chosen token, unverified) and `env['verikloak.bff.selected_peer']` (peer IP selected for trust decisions).
84
- - Provide a structured log hook with `log_with: ->(payload) { logger.info(payload.to_json) }` to consume the same fields emitted to `logger`.
88
+ - Provide a structured log hook with `log_with: ->(payload) { logger.info(payload.to_json) }` to consume the same fields emitted to `logger`. Payload strings are sanitized (control characters removed) before hooks and loggers run to mitigate log forging.
85
89
  - Caution: avoid logging the entire Rack `env` in application logs. Treat `env['verikloak.bff.token']` as sensitive; never emit raw tokens or PII (e.g., emails) to logs.
86
90
 
91
+ - Claims consistency modes
92
+ - 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.
93
+
87
94
  ## Development (for contributors)
88
95
  Clone and install dependencies:
89
96
 
@@ -30,7 +30,8 @@ module Verikloak
30
30
  :enforce_header_consistency, :enforce_claims_consistency,
31
31
  :strip_suspicious_headers, :xff_strategy, :clock_skew_leeway,
32
32
  :logger, :token_header_priority, :peer_preference,
33
- :forwarded_header_name, :auth_request_headers, :log_with
33
+ :forwarded_header_name, :auth_request_headers, :log_with,
34
+ :claims_consistency_mode
34
35
 
35
36
  # enforce_claims_consistency example:
36
37
  # { email: :email, user: :sub, groups: :realm_roles }
@@ -40,6 +41,7 @@ module Verikloak
40
41
  @require_forwarded_header = false
41
42
  @enforce_header_consistency = true
42
43
  @enforce_claims_consistency = {}
44
+ @claims_consistency_mode = :enforce
43
45
  @strip_suspicious_headers = true
44
46
  @xff_strategy = :rightmost
45
47
  @peer_preference = :remote_then_xff
@@ -24,6 +24,61 @@ require 'verikloak/bff/constants'
24
24
 
25
25
  module Verikloak
26
26
  module BFF
27
+ # Internal helpers that sanitize tokens and log payloads for HeaderGuard.
28
+ module HeaderGuardSanitizer
29
+ LOG_CONTROL_CHARS = /[[:cntrl:]]/
30
+
31
+ module_function
32
+
33
+ def token_tags(token)
34
+ return {} unless token
35
+
36
+ payload, header = decode_unverified(token)
37
+ aud = payload['aud']
38
+ aud = aud.join(' ') if aud.is_a?(Array)
39
+ {
40
+ sub: sanitize_log_field(payload['sub']&.to_s),
41
+ iss: sanitize_log_field(payload['iss']&.to_s),
42
+ aud: sanitize_log_field(aud&.to_s),
43
+ kid: sanitize_log_field(header['kid']&.to_s)
44
+ }.compact
45
+ rescue StandardError
46
+ {}
47
+ end
48
+
49
+ def decode_unverified(token)
50
+ return [{}, {}] if token.nil? || token.bytesize > Constants::MAX_TOKEN_BYTES
51
+
52
+ JWT.decode(token, nil, false)
53
+ rescue StandardError
54
+ [{}, {}]
55
+ end
56
+
57
+ def sanitize_payload(payload)
58
+ payload.transform_values { |value| sanitize_log_field(value) }.compact
59
+ end
60
+
61
+ def sanitize_log_field(value)
62
+ case value
63
+ when nil
64
+ nil
65
+ when String
66
+ sanitized = sanitize_string(value)
67
+ sanitized.empty? ? nil : sanitized
68
+ when Array
69
+ sanitized = value.map { |item| item.is_a?(String) ? sanitize_string(item) : item }
70
+ sanitized.reject! { |item| item.nil? || (item.is_a?(String) && item.empty?) }
71
+ sanitized.empty? ? nil : sanitized
72
+ else
73
+ value
74
+ end
75
+ end
76
+
77
+ def sanitize_string(value)
78
+ value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').gsub(LOG_CONTROL_CHARS, '')
79
+ end
80
+ end
81
+
27
82
  # Rack middleware that enforces BFF boundary and header/claims consistency.
28
83
  class HeaderGuard
29
84
  # Accept both Rack 2 and Rack 3 builder call styles:
@@ -106,37 +161,6 @@ module Verikloak
106
161
  end
107
162
  end
108
163
 
109
- # Extract selected JWT tags for audit logging (best-effort, unverified).
110
- #
111
- # @param token [String, nil]
112
- # @return [Hash] subset of {sub, iss, aud, kid}
113
- def token_tags(token)
114
- return {} unless token
115
-
116
- payload, header = decode_unverified(token)
117
- {
118
- sub: payload['sub'],
119
- iss: payload['iss'],
120
- aud: (payload['aud'].is_a?(Array) ? payload['aud'].join(' ') : payload['aud']).to_s,
121
- kid: header['kid']
122
- }.compact
123
- rescue StandardError
124
- {}
125
- end
126
-
127
- # Decode JWT header/payload without validation (for logging only).
128
- #
129
- # @param token [String]
130
- # @return [Array(Hash, Hash)] [payload, header]
131
-
132
- def decode_unverified(token)
133
- return [{}, {}] if token.nil? || token.bytesize > Constants::MAX_TOKEN_BYTES
134
-
135
- JWT.decode(token, nil, false)
136
- rescue StandardError
137
- [{}, {}]
138
- end
139
-
140
164
  # Extract request id for logging from common headers.
141
165
  #
142
166
  # @param env [Hash]
@@ -161,16 +185,17 @@ module Verikloak
161
185
  def log_event(env, kind, **attrs)
162
186
  lg = logger(env)
163
187
  payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
188
+ sanitized = HeaderGuardSanitizer.sanitize_payload(payload)
164
189
  if @config.log_with.respond_to?(:call)
165
190
  begin
166
- @config.log_with.call(payload)
191
+ @config.log_with.call(sanitized)
167
192
  rescue StandardError
168
193
  # ignore log hook failures
169
194
  end
170
195
  end
171
196
  return unless lg
172
197
 
173
- msg = payload.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
198
+ msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
174
199
  level = (kind == :ok ? :info : :warn)
175
200
  lg.public_send(level, msg)
176
201
  rescue StandardError
@@ -221,9 +246,21 @@ module Verikloak
221
246
 
222
247
  field = res.last
223
248
  log_event(env, :claims_mismatch, field: field.to_s)
249
+ return if claims_consistency_log_only?
250
+
224
251
  raise ClaimsMismatchError, field
225
252
  end
226
253
 
254
+ # Determine whether claims mismatches should only be logged.
255
+ #
256
+ # @return [Boolean]
257
+ def claims_consistency_log_only?
258
+ mode = @config.claims_consistency_mode || :enforce
259
+ mode = mode.to_sym if mode.is_a?(String)
260
+ mode = :enforce unless %i[enforce log_only].include?(mode)
261
+ mode == :log_only
262
+ end
263
+
227
264
  # Set normalized Authorization header and emit success log.
228
265
  #
229
266
  # @param env [Hash]
@@ -234,7 +271,7 @@ module Verikloak
234
271
  return unless chosen
235
272
 
236
273
  ForwardedToken.set_authorization!(env, chosen)
237
- log_event(env, :ok, source: token_source(auth_token, fwd_token), **token_tags(chosen))
274
+ log_event(env, :ok, source: token_source(auth_token, fwd_token), **HeaderGuardSanitizer.token_tags(chosen))
238
275
  end
239
276
 
240
277
  # Build a minimal RFC6750-style error response.
@@ -5,6 +5,6 @@
5
5
  # @return [String]
6
6
  module Verikloak
7
7
  module BFF
8
- VERSION = '0.1.1'
8
+ VERSION = '0.1.2'
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.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -95,7 +95,7 @@ metadata:
95
95
  source_code_uri: https://github.com/taiyaky/verikloak-bff
96
96
  changelog_uri: https://github.com/taiyaky/verikloak-bff/blob/main/CHANGELOG.md
97
97
  bug_tracker_uri: https://github.com/taiyaky/verikloak-bff/issues
98
- documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.1.1
98
+ documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.1.2
99
99
  rubygems_mfa_required: 'true'
100
100
  rdoc_options: []
101
101
  require_paths: