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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +8 -1
- data/lib/verikloak/bff/configuration.rb +3 -1
- data/lib/verikloak/bff/header_guard.rb +71 -34
- data/lib/verikloak/bff/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6901b11146849ea25306d9f78b3cf7d8903dce4374efcf1322a7aed6a3d52872
|
|
4
|
+
data.tar.gz: 9a9cd3f59250aa57599de7e973009dedff8d075a395632eef1a19a889fb6257c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
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 =
|
|
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.
|
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.
|
|
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.
|
|
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:
|