verikloak-bff 1.0.0 → 1.1.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 +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/verikloak/bff/configuration.rb +2 -5
- data/lib/verikloak/bff/constants.rb +8 -0
- data/lib/verikloak/bff/forwarded_token.rb +12 -13
- data/lib/verikloak/bff/header_guard.rb +70 -45
- data/lib/verikloak/bff/proxy_trust.rb +25 -18
- data/lib/verikloak/bff/rails.rb +31 -53
- data/lib/verikloak/bff/version.rb +1 -1
- data/lib/verikloak/header_sources.rb +5 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fc1c8dd0dd14c3ae72321e7265c572cd2c5437285325a5ab5ae2836205fc2e1
|
|
4
|
+
data.tar.gz: 51a5f52cf417750da1454bd0bf8ec8635cfa65b3df3b8ff634d7fe556b2b8498
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92ff1528409857382e5530fc6de4c1f20d23deb8888a298b8f610c96cdb7ecf554545e3561ba0ffeada9001bc641fb8aea42f11b3eb595c85732136ff7e912ec
|
|
7
|
+
data.tar.gz: a6faf3c0282d56aa048d2176a213473d716fde00fedab97bd8d4a8fa1e4e811aea9f84d9e14ee6e9ac8a87a2f5f498cb9f1a66efe3c1bcb2882c29152b23d8f5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
Verified against verikloak 1.1.0 (core) and verikloak-rails 1.2.0.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Empty Bearer Authorization**: `ForwardedToken.normalize_auth` now returns `nil` for a Bearer scheme without a token (e.g. `Authorization: Bearer`). Previously it returned an empty string, which caused a spurious `401 header_mismatch` when a valid forwarded token was present. `seed_authorization_if_needed` no longer re-inspects the raw `HTTP_AUTHORIZATION` value either, so a bare `Authorization: Bearer` no longer blocks seeding a valid token from `token_header_priority` headers
|
|
16
|
+
- **Multi-line Authorization hardening**: `ForwardedToken.normalize_auth` is anchored with `\A`/`\z` (previously `^`/`$`) and only accepts space/tab between the scheme and token, so a multi-line value cannot smuggle a `Bearer <token>` line past a non-Bearer first line
|
|
17
|
+
- **`peer_preference` honored in trust decisions**: `HeaderGuard` now passes the configured `peer_preference` (`:remote_then_xff` / `:xff_only`) to `ProxyTrust.trusted?`. Previously the preference only affected the `verikloak.bff.selected_peer` env hint and the trust decision always used `:remote_then_xff`, contradicting the documented behavior
|
|
18
|
+
- **`peer_preference` fail-safe**: an unrecognized or `nil` `peer_preference` now resolves to the safe `REMOTE_ADDR`-first path instead of the client-controlled `X-Forwarded-For`, and `HeaderGuard` raises `ConfigurationError` at startup for an unrecognized value so a typo cannot silently weaken the trust decision
|
|
19
|
+
- **Trusted proxy rule isolation**: a rule that raises (invalid CIDR string, failing Proc) no longer silently disables the remaining `trusted_proxies` rules; each rule is evaluated independently, and the failure is surfaced under `$DEBUG` instead of being swallowed without a trace
|
|
20
|
+
- **Fail-fast IP/CIDR validation**: `HeaderGuard` raises `ConfigurationError` at startup when a `trusted_proxies` string cannot be parsed as an IP or CIDR (previously only CIDR strings containing `/` were checked, so a malformed plain IP such as `10.0.0.999` booted and then silently rejected every request)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Minimum `verikloak` dependency raised to `~> 1.1` (from `~> 1.0`), matching the coordinated 1.1.0 core / 1.2.0 verikloak-rails release; `verikloak-rails` 1.2.0 already requires `verikloak ~> 1.1`
|
|
24
|
+
- **`Verikloak::BFF::Rails::Middleware.insert_before_core` added**: inserts HeaderGuard *before* `Verikloak::Middleware`, matching the documented stack order (`[HeaderGuard] → [Verikloak::Middleware] → [App]`) and the behavior of `verikloak-rails`
|
|
25
|
+
- `ForwardedToken.strip_suspicious!` and `Configuration` now share the default `X-Auth-Request-*` header list via `Constants::DEFAULT_AUTH_REQUEST_HEADERS` (previously duplicated)
|
|
26
|
+
- `HeaderGuard` no longer writes the `Authorization` header twice when seeding from `token_header_priority`; the header is written once during request finalization
|
|
27
|
+
|
|
28
|
+
### Deprecated
|
|
29
|
+
- **`Verikloak::BFF::Rails::Middleware.insert_after_core`**: deprecated in favor of `insert_before_core`. It now emits a deprecation warning and delegates to `insert_before_core`, because inserting HeaderGuard *after* the core middleware let core verification run on un-normalized tokens. The dead `auto_insert_enabled?` / `core_config` checks (which read a `Verikloak.config` that does not exist in the core gem — the real flag lives in `Verikloak::Rails.config`) were removed along the way
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
- Internal helpers `HeaderGuardSanitizer.token_tags` and `HeaderGuardSanitizer.decode_unverified` (unused since token decoding was consolidated into `RequestTokens`; use `Verikloak::BFF::JwtUtils.decode_unverified` directly if needed)
|
|
33
|
+
- `ProxyTrust.from_trusted_proxy?` (unused thin wrapper around `ProxyTrust.trusted?`; call `ProxyTrust.trusted?(env, trusted, :rightmost, preference: ...)` directly)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
10
37
|
## [1.0.0] - 2026-02-15
|
|
11
38
|
|
|
12
39
|
### Fixed
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
# @return [Logger, nil] optional logger for audit tags
|
|
26
26
|
|
|
27
27
|
require 'verikloak/header_sources'
|
|
28
|
+
require 'verikloak/bff/constants'
|
|
28
29
|
|
|
29
30
|
module Verikloak
|
|
30
31
|
module BFF
|
|
@@ -59,11 +60,7 @@ module Verikloak
|
|
|
59
60
|
@logger = nil
|
|
60
61
|
@log_with = nil
|
|
61
62
|
self.forwarded_header_name = Verikloak::HeaderSources::DEFAULT_FORWARDED_HEADER
|
|
62
|
-
@auth_request_headers =
|
|
63
|
-
email: 'HTTP_X_AUTH_REQUEST_EMAIL',
|
|
64
|
-
user: 'HTTP_X_AUTH_REQUEST_USER',
|
|
65
|
-
groups: 'HTTP_X_AUTH_REQUEST_GROUPS'
|
|
66
|
-
}
|
|
63
|
+
@auth_request_headers = Constants::DEFAULT_AUTH_REQUEST_HEADERS.dup
|
|
67
64
|
# When Authorization is empty and no chosen token exists, try these env headers (in order)
|
|
68
65
|
# to seed Authorization, similar to verikloak-rails behavior. HTTP_AUTHORIZATION is always ignored as a source.
|
|
69
66
|
self.token_header_priority = Verikloak::HeaderSources.default_priority(forwarded_header: @forwarded_header_name)
|
|
@@ -16,6 +16,14 @@ module Verikloak
|
|
|
16
16
|
# Maximum length for individual log field values to prevent log flooding
|
|
17
17
|
# from oversized or malicious JWT claims.
|
|
18
18
|
MAX_LOG_FIELD_LENGTH = 256
|
|
19
|
+
|
|
20
|
+
# Default X-Auth-Request-* env keys, shared by {Configuration} defaults
|
|
21
|
+
# and {ForwardedToken.strip_suspicious!} so both stay in sync.
|
|
22
|
+
DEFAULT_AUTH_REQUEST_HEADERS = {
|
|
23
|
+
email: 'HTTP_X_AUTH_REQUEST_EMAIL',
|
|
24
|
+
user: 'HTTP_X_AUTH_REQUEST_USER',
|
|
25
|
+
groups: 'HTTP_X_AUTH_REQUEST_GROUPS'
|
|
26
|
+
}.freeze
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# @see .extract
|
|
6
6
|
|
|
7
7
|
require 'verikloak/header_sources'
|
|
8
|
+
require 'verikloak/bff/constants'
|
|
8
9
|
|
|
9
10
|
module Verikloak
|
|
10
11
|
module BFF
|
|
@@ -28,17 +29,20 @@ module Verikloak
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Only accept Bearer scheme for Authorization header.
|
|
32
|
+
# A scheme without a token (e.g. "Bearer" or "Bearer ") is treated as
|
|
33
|
+
# absent so that an empty Authorization header never shadows a valid
|
|
34
|
+
# forwarded token.
|
|
35
|
+
#
|
|
36
|
+
# The pattern is anchored with \A/\z (not ^/$) so a multi-line value
|
|
37
|
+
# cannot smuggle a "Bearer <token>" line past a non-Bearer first line,
|
|
38
|
+
# and only space/tab may separate the scheme from the token.
|
|
31
39
|
#
|
|
32
40
|
# @param raw [String, nil]
|
|
33
|
-
# @return [String, nil] token or nil when not Bearer
|
|
41
|
+
# @return [String, nil] token or nil when not Bearer or token is empty
|
|
34
42
|
def normalize_auth(raw)
|
|
35
43
|
return nil unless raw
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
return ::Regexp.last_match(1) if token =~ /^Bearer\s+(.+)$/i
|
|
39
|
-
return token[6..] if token =~ /^Bearer(?!\s)/i
|
|
40
|
-
|
|
41
|
-
nil
|
|
45
|
+
raw.to_s.strip[/\ABearer[ \t]*(.+)\z/i, 1]
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
# Accept either bare token or Bearer for forwarded header.
|
|
@@ -105,13 +109,8 @@ module Verikloak
|
|
|
105
109
|
# @param headers [Hash{Symbol=>String}, nil] explicit headers to strip
|
|
106
110
|
# @return [void]
|
|
107
111
|
def strip_suspicious!(env, headers = nil)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
111
|
-
end
|
|
112
|
-
env.delete('HTTP_X_AUTH_REQUEST_EMAIL')
|
|
113
|
-
env.delete('HTTP_X_AUTH_REQUEST_USER')
|
|
114
|
-
env.delete('HTTP_X_AUTH_REQUEST_GROUPS')
|
|
112
|
+
headers = Constants::DEFAULT_AUTH_REQUEST_HEADERS unless headers.is_a?(Hash)
|
|
113
|
+
headers.each_value { |h| env.delete(h) }
|
|
115
114
|
end
|
|
116
115
|
end
|
|
117
116
|
end
|
|
@@ -32,21 +32,6 @@ module Verikloak
|
|
|
32
32
|
|
|
33
33
|
module_function
|
|
34
34
|
|
|
35
|
-
# Generate sanitized token metadata suitable for structured logging without
|
|
36
|
-
# verifying the signature.
|
|
37
|
-
#
|
|
38
|
-
# @param token [String, nil]
|
|
39
|
-
# @return [Hash{Symbol=>Object}] sanitized tags keyed by JWT claim/header
|
|
40
|
-
def token_tags(token)
|
|
41
|
-
return {} unless token
|
|
42
|
-
|
|
43
|
-
payload, header = decode_unverified(token)
|
|
44
|
-
token_tags_from_decoded(payload, header)
|
|
45
|
-
rescue StandardError => e
|
|
46
|
-
warn("[verikloak-bff] token_tags failed: #{e.class}: #{e.message}") if $DEBUG
|
|
47
|
-
{}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
35
|
# Build sanitized log tags from pre-decoded JWT payload and header.
|
|
51
36
|
# Avoids redundant decoding when the caller already has decoded data.
|
|
52
37
|
#
|
|
@@ -69,15 +54,6 @@ module Verikloak
|
|
|
69
54
|
{}
|
|
70
55
|
end
|
|
71
56
|
|
|
72
|
-
# Decode a JWT without verifying the signature while guarding against
|
|
73
|
-
# excessively large tokens.
|
|
74
|
-
#
|
|
75
|
-
# @param token [String, nil]
|
|
76
|
-
# @return [Array<Hash>] payload and header hashes
|
|
77
|
-
def decode_unverified(token)
|
|
78
|
-
Verikloak::BFF::JwtUtils.decode_unverified(token)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
57
|
# Remove unsafe characters from a structured logging payload.
|
|
82
58
|
#
|
|
83
59
|
# @param payload [Hash]
|
|
@@ -212,6 +188,9 @@ module Verikloak
|
|
|
212
188
|
# Error raised when trusted_proxies is not configured and disabled is not explicitly set.
|
|
213
189
|
class ConfigurationError < StandardError; end
|
|
214
190
|
|
|
191
|
+
# Recognized values for the `peer_preference` setting.
|
|
192
|
+
VALID_PEER_PREFERENCES = %i[remote_then_xff xff_only].freeze
|
|
193
|
+
|
|
215
194
|
RequestTokens = Struct.new(:auth, :forwarded, :chosen, :decoded_payload, :decoded_header)
|
|
216
195
|
|
|
217
196
|
# Accept both Rack 2 and Rack 3 builder call styles:
|
|
@@ -277,8 +256,10 @@ module Verikloak
|
|
|
277
256
|
|
|
278
257
|
private
|
|
279
258
|
|
|
280
|
-
# Validate that required configuration is present.
|
|
281
|
-
# Raises ConfigurationError if trusted_proxies is not configured and disabled is false
|
|
259
|
+
# Validate that required configuration is present and well-formed.
|
|
260
|
+
# Raises ConfigurationError if trusted_proxies is not configured and disabled is false,
|
|
261
|
+
# when an IP/CIDR rule cannot be parsed, or when peer_preference is unrecognized
|
|
262
|
+
# (fail-fast instead of silently rejecting every request at runtime).
|
|
282
263
|
#
|
|
283
264
|
# @raise [ConfigurationError]
|
|
284
265
|
# @return [void]
|
|
@@ -286,12 +267,53 @@ module Verikloak
|
|
|
286
267
|
return if @config.disabled
|
|
287
268
|
|
|
288
269
|
proxies = @config.trusted_proxies
|
|
289
|
-
|
|
270
|
+
if proxies.nil? || proxies.empty?
|
|
271
|
+
raise ConfigurationError,
|
|
272
|
+
'trusted_proxies must be configured for Verikloak::BFF::HeaderGuard. ' \
|
|
273
|
+
'Set trusted_proxies to an array of allowed proxy addresses/CIDRs, ' \
|
|
274
|
+
'or set disabled: true to explicitly skip BFF protection.'
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
validate_trusted_proxy_rules!(proxies)
|
|
278
|
+
validate_peer_preference!
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Fail fast on unparsable IP/CIDR strings in trusted_proxies. Both plain
|
|
282
|
+
# IPs (exact-match rules) and CIDRs are checked, since a malformed plain
|
|
283
|
+
# IP would otherwise boot successfully and silently reject every request.
|
|
284
|
+
# Parsing is delegated to ProxyTrust.ip_or_nil so validation and runtime
|
|
285
|
+
# matching stay in lockstep.
|
|
286
|
+
#
|
|
287
|
+
# @param proxies [Array<String, Regexp, Proc>]
|
|
288
|
+
# @raise [ConfigurationError]
|
|
289
|
+
# @return [void]
|
|
290
|
+
def validate_trusted_proxy_rules!(proxies)
|
|
291
|
+
proxies.each do |rule|
|
|
292
|
+
next unless rule.is_a?(String)
|
|
293
|
+
next if ProxyTrust.ip_or_nil(rule)
|
|
294
|
+
|
|
295
|
+
kind = rule.include?('/') ? 'CIDR' : 'IP'
|
|
296
|
+
raise ConfigurationError,
|
|
297
|
+
"invalid #{kind} rule in trusted_proxies: #{rule.inspect}. " \
|
|
298
|
+
'Fix the rule so that proxy trust checks can match it.'
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Fail fast on an unrecognized peer_preference so a typo cannot silently
|
|
303
|
+
# change which peer the trust decision is based on (ProxyTrust treats any
|
|
304
|
+
# non-:xff_only value as the safe REMOTE_ADDR-first path, which would mask
|
|
305
|
+
# an intended :xff_only that was misspelled).
|
|
306
|
+
#
|
|
307
|
+
# @raise [ConfigurationError]
|
|
308
|
+
# @return [void]
|
|
309
|
+
def validate_peer_preference!
|
|
310
|
+
pref = @config.peer_preference
|
|
311
|
+
return if pref.nil?
|
|
312
|
+
return if VALID_PEER_PREFERENCES.include?(pref.to_s.to_sym)
|
|
290
313
|
|
|
291
314
|
raise ConfigurationError,
|
|
292
|
-
|
|
293
|
-
'
|
|
294
|
-
'or set disabled: true to explicitly skip BFF protection.'
|
|
315
|
+
"invalid peer_preference: #{pref.inspect}. " \
|
|
316
|
+
'Expected :remote_then_xff or :xff_only (or nil for the default).'
|
|
295
317
|
end
|
|
296
318
|
|
|
297
319
|
# Build token state by extracting, validating, and selecting the active token.
|
|
@@ -361,11 +383,14 @@ module Verikloak
|
|
|
361
383
|
end
|
|
362
384
|
|
|
363
385
|
# Raise when the request did not come through a trusted proxy.
|
|
386
|
+
# Honors the configured peer_preference (:remote_then_xff or :xff_only)
|
|
387
|
+
# when selecting the peer used for the trust decision.
|
|
364
388
|
#
|
|
365
389
|
# @param env [Hash]
|
|
366
390
|
# @raise [UntrustedProxyError]
|
|
367
391
|
def ensure_trusted_proxy!(env)
|
|
368
|
-
return if ProxyTrust.trusted?(env, @config.trusted_proxies, @config.xff_strategy
|
|
392
|
+
return if ProxyTrust.trusted?(env, @config.trusted_proxies, @config.xff_strategy,
|
|
393
|
+
preference: @config.peer_preference)
|
|
369
394
|
|
|
370
395
|
raise UntrustedProxyError
|
|
371
396
|
end
|
|
@@ -444,35 +469,35 @@ module Verikloak
|
|
|
444
469
|
end
|
|
445
470
|
|
|
446
471
|
# Resolve the first env header from which to source a bearer token.
|
|
447
|
-
#
|
|
472
|
+
# HTTP_AUTHORIZATION is never a source. Proxy trust is already guaranteed
|
|
473
|
+
# by stage 1 ({#ensure_trusted_proxy!}) before this is reached.
|
|
448
474
|
#
|
|
449
475
|
# @param env [Hash]
|
|
450
476
|
# @return [String, nil]
|
|
451
477
|
def resolve_first_token_header(env)
|
|
452
|
-
candidates = Array(@config.token_header_priority)
|
|
453
|
-
candidates -= [Verikloak::HeaderSources::AUTHORIZATION_HEADER]
|
|
454
|
-
fwd_key = @config.forwarded_header_name || Verikloak::HeaderSources::DEFAULT_FORWARDED_HEADER
|
|
455
|
-
if candidates.include?(fwd_key) && !ProxyTrust.from_trusted_proxy?(env, @config.trusted_proxies)
|
|
456
|
-
candidates -= [fwd_key]
|
|
457
|
-
end
|
|
478
|
+
candidates = Array(@config.token_header_priority) - [Verikloak::HeaderSources::AUTHORIZATION_HEADER]
|
|
458
479
|
candidates.find { |k| (v = env[k]) && !v.to_s.empty? }
|
|
459
480
|
end
|
|
460
481
|
|
|
461
|
-
# Seed
|
|
482
|
+
# Seed the chosen token from priority headers when no usable token was
|
|
483
|
+
# chosen. `chosen` is nil only when Authorization held no valid Bearer
|
|
484
|
+
# token (normalize_auth already treats a bare "Bearer" as absent) and no
|
|
485
|
+
# forwarded token was present, so inspecting the raw Authorization header
|
|
486
|
+
# here would re-introduce the empty-Bearer shadowing bug. The Authorization
|
|
487
|
+
# header itself is written once later by {#normalize_authorization!} from
|
|
488
|
+
# the chosen token.
|
|
462
489
|
#
|
|
463
490
|
# @param env [Hash]
|
|
464
491
|
# @param chosen [String, nil]
|
|
465
492
|
# @return [String, nil] possibly updated chosen token
|
|
466
493
|
def seed_authorization_if_needed(env, chosen)
|
|
467
|
-
return chosen unless chosen.nil?
|
|
494
|
+
return chosen unless chosen.nil?
|
|
468
495
|
return chosen unless Array(@config.token_header_priority).any?
|
|
469
496
|
|
|
470
497
|
seeded = resolve_first_token_header(env)
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
end
|
|
475
|
-
chosen
|
|
498
|
+
return chosen unless seeded
|
|
499
|
+
|
|
500
|
+
ForwardedToken.normalize_forwarded(env[seeded]) || env[seeded].to_s
|
|
476
501
|
end
|
|
477
502
|
|
|
478
503
|
# Expose hints to downstream middleware or apps.
|
|
@@ -19,11 +19,13 @@ module Verikloak
|
|
|
19
19
|
# - Regexp: matched against the selected peer IP
|
|
20
20
|
# - Proc: called as `->(ip, env) { ... }` and returns truthy when trusted
|
|
21
21
|
# @param strategy [Symbol, String] `:rightmost` (default) or `:leftmost` for XFF parsing
|
|
22
|
+
# @param preference [Symbol] `:remote_then_xff` (default) prefers REMOTE_ADDR,
|
|
23
|
+
# `:xff_only` selects the peer from X-Forwarded-For first
|
|
22
24
|
# @return [Boolean] true if the selected peer is trusted
|
|
23
25
|
# @example CIDR + Regex allowlist
|
|
24
26
|
# ProxyTrust.trusted?(env, ["10.0.0.0/8", /^192\.168\./], :rightmost)
|
|
25
|
-
def trusted?(env, trusted, strategy = :rightmost)
|
|
26
|
-
remote = resolve_peer(env,
|
|
27
|
+
def trusted?(env, trusted, strategy = :rightmost, preference: :remote_then_xff)
|
|
28
|
+
remote = resolve_peer(env, preference, strategy)
|
|
27
29
|
trusted_remote?(remote, trusted, env)
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -68,6 +70,9 @@ module Verikloak
|
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
# Check whether a single rule trusts the selected remote.
|
|
73
|
+
# A rule that raises (e.g. an invalid CIDR string or a failing Proc) is
|
|
74
|
+
# treated as non-matching so that one bad rule cannot disable the rest
|
|
75
|
+
# of the allowlist.
|
|
71
76
|
#
|
|
72
77
|
# @param rule [String, Regexp, Proc]
|
|
73
78
|
# @param remote [String]
|
|
@@ -90,31 +95,33 @@ module Verikloak
|
|
|
90
95
|
else
|
|
91
96
|
false
|
|
92
97
|
end
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# @param trusted [Array<String, Regexp, Proc>, nil]
|
|
100
|
-
# @return [Boolean]
|
|
101
|
-
def self.from_trusted_proxy?(env, trusted)
|
|
102
|
-
trusted?(env, trusted, :rightmost)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
# Isolate a single failing rule without disabling the rest of the
|
|
100
|
+
# allowlist, but surface it under $DEBUG so a broken Proc/CIDR rule is
|
|
101
|
+
# observable instead of silently denying trust.
|
|
102
|
+
warn("[verikloak-bff] trusted_proxies rule raised and was skipped: #{e.class}: #{e.message}") if $DEBUG
|
|
103
|
+
false
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
# Resolve the peer value based on preference and strategy.
|
|
106
107
|
#
|
|
108
|
+
# Only `:xff_only` selects the peer from X-Forwarded-For first. Every other
|
|
109
|
+
# value — the `:remote_then_xff` default, `nil`, or an unrecognized/typo'd
|
|
110
|
+
# preference — falls back to the safe REMOTE_ADDR-first path so a
|
|
111
|
+
# misconfiguration can never switch the trust decision onto the
|
|
112
|
+
# client-controlled X-Forwarded-For header.
|
|
113
|
+
#
|
|
107
114
|
# @param env [Hash]
|
|
108
115
|
# @param preference [Symbol]
|
|
109
116
|
# @param strategy [Symbol]
|
|
110
117
|
# @return [String, nil]
|
|
111
118
|
def resolve_peer(env, preference, strategy)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
return extract_peer_ip(env, strategy) if preference.to_s.to_sym == :xff_only
|
|
120
|
+
|
|
121
|
+
remote = (env['REMOTE_ADDR'] || '').to_s.strip
|
|
122
|
+
return remote unless remote.empty?
|
|
123
|
+
|
|
124
|
+
# Fall back to X-Forwarded-For when REMOTE_ADDR is empty
|
|
118
125
|
extract_peer_ip(env, strategy)
|
|
119
126
|
end
|
|
120
127
|
|
data/lib/verikloak/bff/rails.rb
CHANGED
|
@@ -6,9 +6,14 @@ module Verikloak
|
|
|
6
6
|
module Rails
|
|
7
7
|
# Middleware management utilities for Rails applications.
|
|
8
8
|
#
|
|
9
|
-
# This module focuses on inserting the HeaderGuard middleware right
|
|
10
|
-
# the core Verikloak middleware
|
|
11
|
-
# not contain the
|
|
9
|
+
# This module focuses on inserting the HeaderGuard middleware right before
|
|
10
|
+
# the core Verikloak middleware (so tokens are normalized before core
|
|
11
|
+
# verification) while gracefully handling stacks that do not contain the
|
|
12
|
+
# core component.
|
|
13
|
+
#
|
|
14
|
+
# NOTE: When `verikloak-rails` is installed, middleware insertion happens
|
|
15
|
+
# automatically via its railtie — these helpers are only needed for manual
|
|
16
|
+
# (non verikloak-rails) setups.
|
|
12
17
|
module Middleware
|
|
13
18
|
module_function
|
|
14
19
|
|
|
@@ -17,12 +22,17 @@ module Verikloak
|
|
|
17
22
|
SKIP_MESSAGE = <<~MSG.chomp.freeze
|
|
18
23
|
[verikloak-bff] Skipping Verikloak::BFF::HeaderGuard insertion because Verikloak::Middleware is not present. Configure verikloak-rails discovery settings and restart once core verification is enabled.
|
|
19
24
|
MSG
|
|
25
|
+
DEPRECATION_MESSAGE = <<~MSG.chomp.freeze
|
|
26
|
+
[verikloak-bff] Verikloak::BFF::Rails::Middleware.insert_after_core is deprecated and will be removed in a future release. Use insert_before_core instead — HeaderGuard must run before Verikloak::Middleware so that tokens are normalized prior to verification. insert_after_core now delegates to insert_before_core.
|
|
27
|
+
MSG
|
|
20
28
|
|
|
21
|
-
# Inserts Verikloak::BFF::HeaderGuard middleware
|
|
29
|
+
# Inserts Verikloak::BFF::HeaderGuard middleware before Verikloak::Middleware
|
|
22
30
|
#
|
|
23
31
|
# Attempts to insert the HeaderGuard middleware into the Rails middleware stack
|
|
24
|
-
#
|
|
25
|
-
#
|
|
32
|
+
# before the core Verikloak::Middleware, matching the documented stack order:
|
|
33
|
+
# [Verikloak::BFF::HeaderGuard] → [Verikloak::Middleware] → [Your App]
|
|
34
|
+
# If the core middleware is not present, logs a warning and gracefully skips
|
|
35
|
+
# the insertion.
|
|
26
36
|
#
|
|
27
37
|
# @param stack [ActionDispatch::MiddlewareStack] Rails middleware stack
|
|
28
38
|
# @param logger [Logger, nil] Optional logger for warning messages
|
|
@@ -30,13 +40,11 @@ module Verikloak
|
|
|
30
40
|
# @raise [RuntimeError] Re-raises non-middleware-related runtime errors
|
|
31
41
|
#
|
|
32
42
|
# @example Inserting middleware in Rails configuration
|
|
33
|
-
# Verikloak::BFF::Rails::Middleware.
|
|
43
|
+
# Verikloak::BFF::Rails::Middleware.insert_before_core(
|
|
34
44
|
# Rails.application.config.middleware,
|
|
35
45
|
# logger: Rails.logger
|
|
36
46
|
# )
|
|
37
|
-
def
|
|
38
|
-
return false unless auto_insert_enabled?
|
|
39
|
-
|
|
47
|
+
def insert_before_core(stack, logger: nil)
|
|
40
48
|
core = core_middleware
|
|
41
49
|
header_guard = header_guard_middleware
|
|
42
50
|
|
|
@@ -45,7 +53,7 @@ module Verikloak
|
|
|
45
53
|
return false
|
|
46
54
|
end
|
|
47
55
|
|
|
48
|
-
stack.
|
|
56
|
+
stack.insert_before(core, header_guard)
|
|
49
57
|
true
|
|
50
58
|
rescue RuntimeError => e
|
|
51
59
|
raise unless missing_core?(e)
|
|
@@ -54,35 +62,18 @@ module Verikloak
|
|
|
54
62
|
false
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# to preserve the previous behavior.
|
|
65
|
+
# @deprecated Use {.insert_before_core} instead. The previous behavior
|
|
66
|
+
# (inserting HeaderGuard *after* the core middleware) contradicted the
|
|
67
|
+
# documented stack order and let core verification see un-normalized
|
|
68
|
+
# tokens. This method now delegates to {.insert_before_core} and emits
|
|
69
|
+
# a deprecation warning.
|
|
63
70
|
#
|
|
64
|
-
# @
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
true
|
|
72
|
-
rescue StandardError
|
|
73
|
-
true
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Returns the Verikloak configuration object when available.
|
|
77
|
-
#
|
|
78
|
-
# @return [Object, nil]
|
|
79
|
-
def core_config
|
|
80
|
-
return nil unless defined?(::Verikloak)
|
|
81
|
-
return nil unless ::Verikloak.respond_to?(:config)
|
|
82
|
-
|
|
83
|
-
::Verikloak.config
|
|
84
|
-
rescue StandardError
|
|
85
|
-
nil
|
|
71
|
+
# @param stack [ActionDispatch::MiddlewareStack] Rails middleware stack
|
|
72
|
+
# @param logger [Logger, nil] Optional logger for warning messages
|
|
73
|
+
# @return [Boolean] true if insertion succeeded, false if skipped
|
|
74
|
+
def insert_after_core(stack, logger: nil)
|
|
75
|
+
logger ? logger.warn(DEPRECATION_MESSAGE) : warn(DEPRECATION_MESSAGE)
|
|
76
|
+
insert_before_core(stack, logger: logger)
|
|
86
77
|
end
|
|
87
78
|
|
|
88
79
|
# Detect whether the Verikloak core middleware is already present in the stack.
|
|
@@ -132,17 +123,10 @@ module Verikloak
|
|
|
132
123
|
# Checks if the error indicates missing core Verikloak middleware
|
|
133
124
|
#
|
|
134
125
|
# Examines a RuntimeError to determine if it was caused by attempting
|
|
135
|
-
# to insert middleware
|
|
126
|
+
# to insert middleware relative to a non-existent Verikloak::Middleware.
|
|
136
127
|
#
|
|
137
128
|
# @param error [RuntimeError] The error to examine
|
|
138
129
|
# @return [Boolean] true if error indicates missing Verikloak::Middleware
|
|
139
|
-
#
|
|
140
|
-
# @example Checking for missing middleware error
|
|
141
|
-
# begin
|
|
142
|
-
# stack.insert_after(::Verikloak::Middleware, SomeMiddleware)
|
|
143
|
-
# rescue RuntimeError => e
|
|
144
|
-
# puts "Missing core!" if missing_core?(e)
|
|
145
|
-
# end
|
|
146
130
|
def missing_core?(error)
|
|
147
131
|
error.message.include?('No such middleware') &&
|
|
148
132
|
error.message.include?(CORE_NAME)
|
|
@@ -155,12 +139,6 @@ module Verikloak
|
|
|
155
139
|
# Uses the provided logger if available, otherwise falls back to warn().
|
|
156
140
|
#
|
|
157
141
|
# @param logger [Logger, nil] Optional logger instance for structured logging
|
|
158
|
-
#
|
|
159
|
-
# @example Logging with Rails logger
|
|
160
|
-
# log_skip(Rails.logger)
|
|
161
|
-
#
|
|
162
|
-
# @example Logging without logger (uses warn)
|
|
163
|
-
# log_skip(nil)
|
|
164
142
|
def log_skip(logger)
|
|
165
143
|
logger ? logger.warn(SKIP_MESSAGE) : warn(SKIP_MESSAGE)
|
|
166
144
|
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Helpers shared across verikloak middleware for normalizing Rack env header
|
|
4
|
-
# names and token source priority lists.
|
|
5
|
-
#
|
|
4
|
+
# names and token source priority lists.
|
|
5
|
+
#
|
|
6
|
+
# NOTE: This file lives under the shared `Verikloak::` namespace but is
|
|
7
|
+
# currently shipped by verikloak-bff only. Moving it into the core `verikloak`
|
|
8
|
+
# gem is planned so that sibling gems can share it without load-path conflicts.
|
|
6
9
|
module Verikloak
|
|
7
10
|
# Provides normalization helpers for Rack env header keys and token priority lists.
|
|
8
11
|
module HeaderSources
|
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: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -55,14 +55,14 @@ dependencies:
|
|
|
55
55
|
requirements:
|
|
56
56
|
- - "~>"
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: '1.
|
|
58
|
+
version: '1.1'
|
|
59
59
|
type: :runtime
|
|
60
60
|
prerelease: false
|
|
61
61
|
version_requirements: !ruby/object:Gem::Requirement
|
|
62
62
|
requirements:
|
|
63
63
|
- - "~>"
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
|
-
version: '1.
|
|
65
|
+
version: '1.1'
|
|
66
66
|
description: Framework-agnostic Rack middleware that normalizes forwarded tokens,
|
|
67
67
|
enforces trust boundaries, and checks header/claims consistency before verikloak.
|
|
68
68
|
executables: []
|
|
@@ -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/1.
|
|
98
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-bff/1.1.0
|
|
99
99
|
rubygems_mfa_required: 'true'
|
|
100
100
|
rdoc_options: []
|
|
101
101
|
require_paths:
|