verikloak-bff 0.1.2 → 0.2.1

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: 6901b11146849ea25306d9f78b3cf7d8903dce4374efcf1322a7aed6a3d52872
4
- data.tar.gz: 9a9cd3f59250aa57599de7e973009dedff8d075a395632eef1a19a889fb6257c
3
+ metadata.gz: 255d2d606e30f92f8c562d91d975be06975a7ba373bd607aa5057e5abcc279a6
4
+ data.tar.gz: 6f81b6407f5a1a6e5d307cb65981f48e12c5356ec411ba05d21829681bedb084
5
5
  SHA512:
6
- metadata.gz: eed7006bf01e9a51b4824d8e4fdd2a759210270fdf5fdb07617a21b5b6e40862232dc162f8bec8e65fdb89cea59443eee7a030c7fdd2e335446c182b2745936a
7
- data.tar.gz: d89ca6f1f4032198704a0167d1c3cad7b8b2b1adc68948bb06b0abbf45c4f51157e2d00f3906ad070c4c59c56ed7ddb31bd8f8a58cd06884304b890889685b91
6
+ metadata.gz: 995250cf2096de074988c0a00d4c2de0ff4b03c798089acfd0f0e0e5e3c0cdc3717da82f4f85be74db68fe40ced3cda5c27ae9f14ae41385a38a24e2eb9c4814
7
+ data.tar.gz: 157ddfbc41d48b9903b59f732ef8128a4d27127fee098d4a6e2388ca713ce15a749acad287b4995f87515d70233f74d4f44f7ac9d289de34d548d26bef3e9d17
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.1] - 2025-09-23
11
+
12
+ ### Fixed
13
+ - Skip inserting `Verikloak::BFF::HeaderGuard` in Rails when `Verikloak::Middleware` is absent (e.g., discovery not configured)
14
+ so that generators and boot sequences no longer fail.
15
+
16
+ ## [0.2.0] - 2025-09-22
17
+
18
+ ### Added
19
+ - `Verikloak::HeaderSources` module for shared header normalization (consumable by verikloak-rails and other adapters).
20
+
21
+ ### Changed
22
+ - `Configuration#token_header_priority=` now normalizes and deduplicates entries, reusing the shared helper and ignoring `HTTP_AUTHORIZATION` automatically.
23
+ - `forwarded_header_name` assignments trigger re-normalization of token priority lists to keep middleware aligned across gems.
24
+
10
25
  ## [0.1.2] - 2025-09-21
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -23,8 +23,22 @@ bundle add verikloak-bff
23
23
 
24
24
  ## Usage
25
25
 
26
- - Rack-only apps: `use Verikloak::BFF::HeaderGuard` before your core Verikloak middleware.
27
- - Rails apps: see the full guide in [docs/rails.md](docs/rails.md) (Gemfile, middleware order、initializer、proxy examples)。
26
+ ### Rack Applications
27
+ Add to your `config.ru`:
28
+ ```ruby
29
+ use Verikloak::BFF::HeaderGuard, trusted_proxies: ['127.0.0.1', '10.0.0.0/8']
30
+ # Place before your core Verikloak middleware
31
+ ```
32
+
33
+ ### Rails Applications
34
+ Simply add to your Gemfile and the middleware will be automatically integrated:
35
+ ```ruby
36
+ gem 'verikloak-bff'
37
+ ```
38
+
39
+ The gem automatically inserts `Verikloak::BFF::HeaderGuard` into the Rails middleware stack after the core `Verikloak::Middleware`. If the core middleware is not present (e.g., discovery not configured), it gracefully skips insertion with a warning, allowing Rails to boot normally.
40
+
41
+ For detailed configuration, proxy setup examples, and troubleshooting, see [docs/rails.md](docs/rails.md).
28
42
 
29
43
  ## Consistency mapping
30
44
 
@@ -53,7 +67,7 @@ See `examples/rack.ru` for a tiny Rack app demo.
53
67
  | `peer_preference` | Symbol (`:remote_then_xff`/`:xff_only`) | `:remote_then_xff` | Whether to prefer `REMOTE_ADDR` before falling back to XFF. |
54
68
  | `clock_skew_leeway` | Integer (seconds) | `30` | Reserved for small exp/nbf skew handled by core verifier. |
55
69
  | `logger` | `Logger` or `nil` | `nil` | Logger for audit tags (`rid`, `sub`, `kid`, `iss/aud`). |
56
- | `token_header_priority` | Array[String] | `['HTTP_X_FORWARDED_ACCESS_TOKEN']` | When Authorization is empty and no token chosen, seed it from these env headers in order. `HTTP_AUTHORIZATION` is ignored as a source; forwarded header is considered only from trusted peers. |
70
+ | `token_header_priority` | Array[String] | `['HTTP_X_FORWARDED_ACCESS_TOKEN']` | When Authorization is empty and no token chosen, seed it from these env headers in order. Values are normalized via `Verikloak::HeaderSources`; `HTTP_AUTHORIZATION` is ignored as a source. |
57
71
  | `forwarded_header_name` | String | `HTTP_X_FORWARDED_ACCESS_TOKEN` | Env key for forwarded access token. |
58
72
  | `auth_request_headers` | Hash | see code | Mapping for `X-Auth-Request-*` env keys: `{ email, user, groups }`. |
59
73
 
@@ -82,6 +96,7 @@ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/r
82
96
  - Authorization seeding from priority headers
83
97
  - When no token is chosen and `HTTP_AUTHORIZATION` is empty, the middleware consults `token_header_priority` to seed Authorization.
84
98
  - `HTTP_AUTHORIZATION` itself is never used as a source; forwarded headers are considered only from trusted peers.
99
+ - Other gems can `require 'verikloak/header_sources'` to reuse the same normalization helpers when sharing configuration defaults.
85
100
 
86
101
  - Observability helpers
87
102
  - Downstream can inspect `env['verikloak.bff.token']` (chosen token, unverified) and `env['verikloak.bff.selected_peer']` (peer IP selected for trust decisions).
@@ -22,6 +22,8 @@
22
22
  # @!attribute [rw] logger
23
23
  # @return [Logger, nil] optional logger for audit tags
24
24
 
25
+ require 'verikloak/header_sources'
26
+
25
27
  module Verikloak
26
28
  module BFF
27
29
  # Configuration for Verikloak::BFF middleware (trusted proxies, header policies, logging, etc.).
@@ -29,12 +31,17 @@ module Verikloak
29
31
  attr_accessor :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
30
32
  :enforce_header_consistency, :enforce_claims_consistency,
31
33
  :strip_suspicious_headers, :xff_strategy, :clock_skew_leeway,
32
- :logger, :token_header_priority, :peer_preference,
33
- :forwarded_header_name, :auth_request_headers, :log_with,
34
+ :logger, :peer_preference, :auth_request_headers, :log_with,
34
35
  :claims_consistency_mode
36
+ attr_reader :token_header_priority, :forwarded_header_name
35
37
 
36
38
  # enforce_claims_consistency example:
37
39
  # { email: :email, user: :sub, groups: :realm_roles }
40
+ #
41
+ # Initialize configuration with secure defaults for proxy trust, token
42
+ # handling, and logging behavior.
43
+ #
44
+ # @return [void]
38
45
  def initialize
39
46
  @trusted_proxies = []
40
47
  @prefer_forwarded = true
@@ -48,7 +55,7 @@ module Verikloak
48
55
  @clock_skew_leeway = 30
49
56
  @logger = nil
50
57
  @log_with = nil
51
- @forwarded_header_name = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
58
+ self.forwarded_header_name = Verikloak::HeaderSources::DEFAULT_FORWARDED_HEADER
52
59
  @auth_request_headers = {
53
60
  email: 'HTTP_X_AUTH_REQUEST_EMAIL',
54
61
  user: 'HTTP_X_AUTH_REQUEST_USER',
@@ -56,9 +63,37 @@ module Verikloak
56
63
  }
57
64
  # When Authorization is empty and no chosen token exists, try these env headers (in order)
58
65
  # to seed Authorization, similar to verikloak-rails behavior. HTTP_AUTHORIZATION is always ignored as a source.
59
- @token_header_priority = %w[
60
- HTTP_X_FORWARDED_ACCESS_TOKEN
61
- ]
66
+ self.token_header_priority = Verikloak::HeaderSources.default_priority(forwarded_header: @forwarded_header_name)
67
+ end
68
+
69
+ # Override forwarded header name while re-normalizing the token priority list.
70
+ # When the forwarded header changes, downstream priority normalization must
71
+ # be refreshed because the forwarded value participates in that list.
72
+ #
73
+ # @param header [String, Symbol]
74
+ # @return [void]
75
+ def forwarded_header_name=(header)
76
+ @forwarded_header_name = Verikloak::HeaderSources.normalize_env_key(header)
77
+ renormalize_token_priority!
78
+ end
79
+
80
+ # Assign token header priority list using shared normalization logic.
81
+ #
82
+ # @param priority [Array<String, Symbol>, String, Symbol, nil]
83
+ # @return [void]
84
+ def token_header_priority=(priority)
85
+ normalized, = Verikloak::HeaderSources.normalize_priority(priority, forwarded_header: @forwarded_header_name)
86
+ @token_header_priority = normalized
87
+ end
88
+
89
+ private
90
+
91
+ # Re-apply token header normalization so it reflects the current forwarded header.
92
+ #
93
+ # @return [void]
94
+ def renormalize_token_priority!
95
+ # Refresh the normalized list so it reflects the new forwarded header name.
96
+ self.token_header_priority = @token_header_priority
62
97
  end
63
98
  end
64
99
  end
@@ -15,12 +15,11 @@ module Verikloak
15
15
  module ConsistencyChecks
16
16
  module_function
17
17
 
18
- # Parse JWT payload without verifying (verikloak core will verify later).
19
- # Decode JWT payload without verification.
18
+ # Decode the JWT payload without verifying the signature. Intended only
19
+ # for lightweight claim comparisons; full verification occurs downstream.
20
20
  #
21
21
  # @param token [String, nil]
22
22
  # @return [Hash] claims or empty hash on error
23
-
24
23
  def decode_claims(token)
25
24
  return {} unless token
26
25
  return {} if token.bytesize > Constants::MAX_TOKEN_BYTES
@@ -36,6 +35,7 @@ module Verikloak
36
35
  # @param env [Hash]
37
36
  # @param token [String, nil]
38
37
  # @param mapping [Hash] e.g., { email: :email, user: :sub, groups: :realm_roles }
38
+ # @param headers_map [Hash{Symbol=>String}, nil] overrides for header keys
39
39
  # @return [true, Array(:error, Symbol)] true or error tuple with failing field
40
40
  def enforce!(env, token, mapping, headers_map = nil)
41
41
  return true if mapping.nil? || mapping.empty?
@@ -63,6 +63,7 @@ module Verikloak
63
63
  #
64
64
  # @param env [Hash]
65
65
  # @param key [Symbol]
66
+ # @param headers_map [Hash{Symbol=>String}, nil]
66
67
  # @return [String, nil]
67
68
  def extract_header_value(env, key, headers_map = nil)
68
69
  return env[headers_map[key]] if headers_map && headers_map[key]
@@ -12,6 +12,12 @@ module Verikloak
12
12
  class Error < StandardError
13
13
  attr_reader :code, :http_status
14
14
 
15
+ # Build a BFF error with a stable code and HTTP status.
16
+ #
17
+ # @param message [String, nil]
18
+ # @param code [String]
19
+ # @param http_status [Integer]
20
+ # @return [void]
15
21
  def initialize(message = nil, code: 'bff_error', http_status: 401)
16
22
  super(message || code)
17
23
  @code = code
@@ -21,6 +27,8 @@ module Verikloak
21
27
 
22
28
  # Raised when a request did not pass through a trusted proxy peer.
23
29
  class UntrustedProxyError < Error
30
+ # @param msg [String]
31
+ # @return [void]
24
32
  def initialize(msg = 'request did not pass through a trusted proxy')
25
33
  super(msg, code: 'untrusted_proxy', http_status: 401)
26
34
  end
@@ -28,6 +36,8 @@ module Verikloak
28
36
 
29
37
  # Raised when require_forwarded_header is enabled but the forwarded token is absent.
30
38
  class MissingForwardedTokenError < Error
39
+ # @param msg [String]
40
+ # @return [void]
31
41
  def initialize(msg = 'missing X-Forwarded-Access-Token')
32
42
  super(msg, code: 'missing_forwarded_token', http_status: 401)
33
43
  end
@@ -35,6 +45,8 @@ module Verikloak
35
45
 
36
46
  # Raised when Authorization and X-Forwarded-Access-Token both exist and differ.
37
47
  class HeaderMismatchError < Error
48
+ # @param msg [String]
49
+ # @return [void]
38
50
  def initialize(msg = 'authorization and forwarded token mismatch')
39
51
  super(msg, code: 'header_mismatch', http_status: 401)
40
52
  end
@@ -42,6 +54,9 @@ module Verikloak
42
54
 
43
55
  # Raised when X-Auth-Request-* headers conflict with JWT claims.
44
56
  class ClaimsMismatchError < Error
57
+ # @param field [Symbol, String]
58
+ # @param msg [String, nil]
59
+ # @return [void]
45
60
  def initialize(field, msg = nil)
46
61
  super(msg || "claims/header mismatch for #{field}", code: 'claims_mismatch', http_status: 403)
47
62
  end
@@ -16,6 +16,8 @@ module Verikloak
16
16
  # Extract normalized tokens from the Rack env.
17
17
  #
18
18
  # @param env [Hash]
19
+ # @param forwarded_header_name [String] Rack env key for forwarded header
20
+ # @param auth_header_name [String] Rack env key for Authorization header
19
21
  # @return [Array(String, String)] [auth_token, forwarded_token]
20
22
  def extract(env, forwarded_header_name = FORWARDED_HEADER, auth_header_name = AUTH_HEADER)
21
23
  fwd_raw = env[forwarded_header_name]
@@ -54,6 +56,7 @@ module Verikloak
54
56
  # - Detects scheme case-insensitively
55
57
  # - Inserts a missing space (e.g., 'BearerXYZ' => 'Bearer XYZ')
56
58
  # - Collapses multiple spaces/tabs after the scheme to a single space
59
+ #
57
60
  # @param token [String]
58
61
  # @return [String]
59
62
  def ensure_bearer(token)
@@ -78,6 +81,7 @@ module Verikloak
78
81
  #
79
82
  # @param env [Hash]
80
83
  # @param token [String]
84
+ # @return [void]
81
85
  def set_authorization!(env, token)
82
86
  existing = env[AUTH_HEADER].to_s
83
87
  # Overwrite only if Authorization is empty or not a valid Bearer value
@@ -98,6 +102,8 @@ module Verikloak
98
102
  # downstream when not emitted by a trusted proxy.
99
103
  #
100
104
  # @param env [Hash]
105
+ # @param headers [Hash{Symbol=>String}, nil] explicit headers to strip
106
+ # @return [void]
101
107
  def strip_suspicious!(env, headers = nil)
102
108
  if headers.is_a?(Hash)
103
109
  headers.each_value { |h| env.delete(h) }
@@ -15,6 +15,7 @@ require 'rack/utils'
15
15
  require 'json'
16
16
  require 'jwt'
17
17
  require 'digest'
18
+ require 'verikloak/header_sources'
18
19
  require 'verikloak/bff/configuration'
19
20
  require 'verikloak/bff/errors'
20
21
  require 'verikloak/bff/proxy_trust'
@@ -30,6 +31,11 @@ module Verikloak
30
31
 
31
32
  module_function
32
33
 
34
+ # Generate sanitized token metadata suitable for structured logging without
35
+ # verifying the signature.
36
+ #
37
+ # @param token [String, nil]
38
+ # @return [Hash{Symbol=>Object}] sanitized tags keyed by JWT claim/header
33
39
  def token_tags(token)
34
40
  return {} unless token
35
41
 
@@ -46,6 +52,11 @@ module Verikloak
46
52
  {}
47
53
  end
48
54
 
55
+ # Decode a JWT without verifying the signature while guarding against
56
+ # excessively large tokens.
57
+ #
58
+ # @param token [String, nil]
59
+ # @return [Array<Hash>] payload and header hashes
49
60
  def decode_unverified(token)
50
61
  return [{}, {}] if token.nil? || token.bytesize > Constants::MAX_TOKEN_BYTES
51
62
 
@@ -54,10 +65,18 @@ module Verikloak
54
65
  [{}, {}]
55
66
  end
56
67
 
68
+ # Remove unsafe characters from a structured logging payload.
69
+ #
70
+ # @param payload [Hash]
71
+ # @return [Hash] sanitized payload suitable for logging
57
72
  def sanitize_payload(payload)
58
73
  payload.transform_values { |value| sanitize_log_field(value) }.compact
59
74
  end
60
75
 
76
+ # Sanitize an individual value destined for logs, pruning empty results.
77
+ #
78
+ # @param value [Object]
79
+ # @return [Object, nil] sanitized value or nil when the result is empty
61
80
  def sanitize_log_field(value)
62
81
  case value
63
82
  when nil
@@ -74,6 +93,10 @@ module Verikloak
74
93
  end
75
94
  end
76
95
 
96
+ # Remove control characters and invalid UTF-8 from a string.
97
+ #
98
+ # @param value [#to_s]
99
+ # @return [String]
77
100
  def sanitize_string(value)
78
101
  value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').gsub(LOG_CONTROL_CHARS, '')
79
102
  end
@@ -128,6 +151,7 @@ module Verikloak
128
151
  # Apply per-instance configuration overrides.
129
152
  #
130
153
  # @param opts [Hash]
154
+ # @return [void]
131
155
  def apply_overrides!(opts)
132
156
  cfg = @config
133
157
  opts.each do |k, v|
@@ -182,6 +206,7 @@ module Verikloak
182
206
  # @param env [Hash]
183
207
  # @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
184
208
  # @param attrs [Hash]
209
+ # @return [void]
185
210
  def log_event(env, kind, **attrs)
186
211
  lg = logger(env)
187
212
  payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
@@ -205,6 +230,7 @@ module Verikloak
205
230
  # Raise when the request did not come through a trusted proxy.
206
231
  #
207
232
  # @param env [Hash]
233
+ # @raise [UntrustedProxyError]
208
234
  def ensure_trusted_proxy!(env)
209
235
  return if ProxyTrust.trusted?(env, @config.trusted_proxies, @config.xff_strategy)
210
236
 
@@ -214,6 +240,7 @@ module Verikloak
214
240
  # Enforce presence of forwarded token when required.
215
241
  #
216
242
  # @param fwd_token [String, nil]
243
+ # @raise [MissingForwardedTokenError]
217
244
  def ensure_forwarded_if_required!(fwd_token)
218
245
  return unless @config.require_forwarded_header
219
246
  raise MissingForwardedTokenError if fwd_token.nil? || fwd_token.to_s.strip.empty?
@@ -224,6 +251,7 @@ module Verikloak
224
251
  # @param env [Hash]
225
252
  # @param auth_token [String, nil]
226
253
  # @param fwd_token [String, nil]
254
+ # @raise [HeaderMismatchError]
227
255
  def enforce_header_consistency!(env, auth_token, fwd_token)
228
256
  return unless @config.enforce_header_consistency
229
257
  return unless auth_token && fwd_token
@@ -240,6 +268,8 @@ module Verikloak
240
268
  #
241
269
  # @param env [Hash]
242
270
  # @param chosen [String, nil]
271
+ # @return [void]
272
+ # @raise [ClaimsMismatchError]
243
273
  def enforce_claims_consistency!(env, chosen)
244
274
  res = ConsistencyChecks.enforce!(env, chosen, @config.enforce_claims_consistency, @config.auth_request_headers)
245
275
  return unless res.is_a?(Array) && res.first == :error
@@ -267,6 +297,7 @@ module Verikloak
267
297
  # @param chosen [String, nil]
268
298
  # @param auth_token [String, nil]
269
299
  # @param fwd_token [String, nil]
300
+ # @return [void]
270
301
  def normalize_authorization!(env, chosen, auth_token, fwd_token)
271
302
  return unless chosen
272
303
 
@@ -289,19 +320,21 @@ module Verikloak
289
320
 
290
321
  # Resolve the first env header from which to source a bearer token.
291
322
  # Forwarded is considered only when the peer is trusted; HTTP_AUTHORIZATION is never a source.
323
+ #
292
324
  # @param env [Hash]
293
325
  # @return [String, nil]
294
326
  def resolve_first_token_header(env)
295
327
  candidates = Array(@config.token_header_priority).dup
296
- candidates -= ['HTTP_AUTHORIZATION']
297
- fwd_key = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
328
+ candidates -= [Verikloak::HeaderSources::AUTHORIZATION_HEADER]
329
+ fwd_key = @config.forwarded_header_name || Verikloak::HeaderSources::DEFAULT_FORWARDED_HEADER
298
330
  if candidates.include?(fwd_key) && !ProxyTrust.from_trusted_proxy?(env, @config.trusted_proxies)
299
331
  candidates -= [fwd_key]
300
332
  end
301
333
  candidates.find { |k| (v = env[k]) && !v.to_s.empty? }
302
334
  end
303
335
 
304
- # Seed Authorization from priority headers if nothing chosen and empty Authorization
336
+ # Seed Authorization from priority headers if nothing chosen and empty Authorization.
337
+ #
305
338
  # @param env [Hash]
306
339
  # @param chosen [String, nil]
307
340
  # @return [String, nil] possibly updated chosen token
@@ -317,9 +350,11 @@ module Verikloak
317
350
  chosen
318
351
  end
319
352
 
320
- # Expose hints to downstream
353
+ # Expose hints to downstream middleware or apps.
354
+ #
321
355
  # @param env [Hash]
322
356
  # @param chosen [String, nil]
357
+ # @return [void]
323
358
  def expose_env_hints(env, chosen)
324
359
  env['verikloak.bff.token'] = chosen if chosen
325
360
  env['verikloak.bff.selected_peer'] =
@@ -53,6 +53,7 @@ module Verikloak
53
53
  end
54
54
 
55
55
  # Return the selected peer IP according to preference and strategy.
56
+ #
56
57
  # @param env [Hash]
57
58
  # @param preference [Symbol] :remote_then_xff or :xff_only
58
59
  # @param strategy [Symbol] :rightmost or :leftmost
@@ -70,6 +71,7 @@ module Verikloak
70
71
  end
71
72
 
72
73
  # Parse string to IPAddr or nil on failure.
74
+ #
73
75
  # @param str [String]
74
76
  # @return [IPAddr, nil]
75
77
  def ip_or_nil(str)
@@ -79,6 +81,7 @@ module Verikloak
79
81
  end
80
82
 
81
83
  # Check whether a single rule trusts the selected remote.
84
+ #
82
85
  # @param rule [String, Regexp, Proc]
83
86
  # @param remote [String]
84
87
  # @param remote_ip [IPAddr, nil]
@@ -104,6 +107,7 @@ module Verikloak
104
107
 
105
108
  # Determine if the request originates from a trusted proxy subnet.
106
109
  # Rails-aligned behavior: prefer REMOTE_ADDR, fallback to nearest (rightmost) X-Forwarded-For.
110
+ #
107
111
  # @param env [Hash]
108
112
  # @param trusted [Array<String, Regexp, Proc>, nil]
109
113
  # @return [Boolean]
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verikloak
4
+ module BFF
5
+ # Rails-specific functionality for Verikloak BFF
6
+ module Rails
7
+ # Middleware management utilities for Rails applications
8
+ #
9
+ # This module provides functionality to insert Verikloak BFF middleware
10
+ # into Rails middleware stack, with proper error handling for cases where
11
+ # the core Verikloak middleware is not present.
12
+ module Middleware
13
+ module_function
14
+
15
+ # Inserts Verikloak::BFF::HeaderGuard middleware after Verikloak::Middleware
16
+ #
17
+ # Attempts to insert the HeaderGuard middleware into the Rails middleware stack
18
+ # after the core Verikloak::Middleware. If the core middleware is not present,
19
+ # logs a warning and gracefully skips the insertion.
20
+ #
21
+ # @param stack [ActionDispatch::MiddlewareStack] Rails middleware stack
22
+ # @param logger [Logger, nil] Optional logger for warning messages
23
+ # @return [Boolean] true if insertion succeeded, false if skipped due to missing core
24
+ # @raise [RuntimeError] Re-raises non-middleware-related runtime errors
25
+ #
26
+ # @example Inserting middleware in Rails configuration
27
+ # Verikloak::BFF::Rails::Middleware.insert_after_core(
28
+ # Rails.application.config.middleware,
29
+ # logger: Rails.logger
30
+ # )
31
+ def insert_after_core(stack, logger: nil)
32
+ stack.insert_after(::Verikloak::Middleware, ::Verikloak::BFF::HeaderGuard)
33
+ true
34
+ rescue RuntimeError => e
35
+ raise unless missing_core?(e)
36
+
37
+ log_skip(logger)
38
+ false
39
+ end
40
+
41
+ # Checks if the error indicates missing core Verikloak middleware
42
+ #
43
+ # Examines a RuntimeError to determine if it was caused by attempting
44
+ # to insert middleware after a non-existent Verikloak::Middleware.
45
+ #
46
+ # @param error [RuntimeError] The error to examine
47
+ # @return [Boolean] true if error indicates missing Verikloak::Middleware
48
+ #
49
+ # @example Checking for missing middleware error
50
+ # begin
51
+ # stack.insert_after(::Verikloak::Middleware, SomeMiddleware)
52
+ # rescue RuntimeError => e
53
+ # puts "Missing core!" if missing_core?(e)
54
+ # end
55
+ def missing_core?(error)
56
+ error.message.include?('No such middleware') &&
57
+ error.message.include?('Verikloak::Middleware')
58
+ end
59
+
60
+ # Logs a warning message about skipping middleware insertion
61
+ #
62
+ # Outputs a descriptive warning message explaining why the HeaderGuard
63
+ # middleware insertion was skipped and provides guidance for resolution.
64
+ # Uses the provided logger if available, otherwise falls back to warn().
65
+ #
66
+ # @param logger [Logger, nil] Optional logger instance for structured logging
67
+ #
68
+ # @example Logging with Rails logger
69
+ # log_skip(Rails.logger)
70
+ #
71
+ # @example Logging without logger (uses warn)
72
+ # log_skip(nil)
73
+ def log_skip(logger)
74
+ message = <<~MSG.chomp
75
+ [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.
76
+ MSG
77
+
78
+ if logger
79
+ logger.warn(message)
80
+ else
81
+ warn(message)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rails'
4
+
5
+ module Verikloak
6
+ # Module providing Verikloak BFF (Backend for Frontend) functionality
7
+ module BFF
8
+ # Railtie class for integrating Verikloak BFF middleware into Rails applications
9
+ #
10
+ # This class automatically inserts Verikloak::BFF::Rails::Middleware into the
11
+ # Rails initialization process. The middleware is inserted after the
12
+ # 'verikloak.middleware' initializer and configured with an appropriate logger.
13
+ #
14
+ # @example Automatic initialization in Rails applications
15
+ # # Simply adding verikloak-bff to Gemfile automatically enables it
16
+ # gem 'verikloak-bff'
17
+ #
18
+ # @see Verikloak::BFF::Rails::Middleware
19
+ class Railtie < ::Rails::Railtie
20
+ # Initializer that inserts Verikloak BFF middleware into Rails application
21
+ #
22
+ # This initializer runs after 'verikloak.middleware' and inserts
23
+ # Verikloak::BFF::Rails::Middleware at the appropriate position.
24
+ # Uses Rails.logger as the logger if available.
25
+ #
26
+ # @param app [Rails::Application] Rails application instance
27
+ initializer 'verikloak.bff.insert_middleware', after: 'verikloak.middleware' do |app|
28
+ logger = ::Rails.logger if defined?(::Rails.logger)
29
+
30
+ Verikloak::BFF::Rails::Middleware.insert_after_core(app.config.middleware, logger: logger)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -5,6 +5,6 @@
5
5
  # @return [String]
6
6
  module Verikloak
7
7
  module BFF
8
- VERSION = '0.1.2'
8
+ VERSION = '0.2.1'
9
9
  end
10
10
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers shared across verikloak middleware for normalizing Rack env header
4
+ # names and token source priority lists. Extracted to allow other gems (such as
5
+ # verikloak-rails) to consume the same normalization logic.
6
+ module Verikloak
7
+ # Provides normalization helpers for Rack env header keys and token priority lists.
8
+ module HeaderSources
9
+ module_function
10
+
11
+ DEFAULT_FORWARDED_HEADER = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
12
+ AUTHORIZATION_HEADER = 'HTTP_AUTHORIZATION'
13
+
14
+ # Normalize a Rack env header key, accepting symbols, mixed case, or dash
15
+ # separated names and returning an upper-case HTTP_* variant.
16
+ #
17
+ # @param header [String, Symbol, nil]
18
+ # @return [String] normalized env key or empty string when blank
19
+ def normalize_env_key(header)
20
+ key = header.to_s.strip
21
+ return '' if key.empty?
22
+
23
+ key = key.tr('-', '_').upcase
24
+ key = "HTTP_#{key}" unless key.start_with?('HTTP_')
25
+ key
26
+ end
27
+
28
+ # Normalize a token priority list by stripping blanks, rejecting
29
+ # Authorization as a source, and deduplicating entries while preserving
30
+ # order.
31
+ #
32
+ # @param priority [Array<String, Symbol>, String, Symbol, nil]
33
+ # @param forwarded_header [String, Symbol]
34
+ # @param drop_authorization [Boolean]
35
+ # @return [Array(Array<String>, String)] normalized priority and forwarded key
36
+ def normalize_priority(priority, forwarded_header: DEFAULT_FORWARDED_HEADER, drop_authorization: true)
37
+ forwarded_env = normalize_env_key(forwarded_header)
38
+ items = Array(priority).flatten
39
+
40
+ normalized = items.map { |value| normalize_env_key(value) }.reject(&:empty?)
41
+ normalized = [forwarded_env] if normalized.empty?
42
+ normalized = normalized.reject { |key| drop_authorization && key == AUTHORIZATION_HEADER }
43
+
44
+ deduped = []
45
+ normalized.each do |key|
46
+ deduped << key unless deduped.include?(key)
47
+ end
48
+
49
+ [deduped.freeze, forwarded_env]
50
+ end
51
+
52
+ # Default priority list using the provided forwarded header name.
53
+ #
54
+ # @param forwarded_header [String, Symbol]
55
+ # @return [Array<String>] normalized default priority
56
+ def default_priority(forwarded_header: DEFAULT_FORWARDED_HEADER)
57
+ normalize_priority([forwarded_header], forwarded_header: forwarded_header).first
58
+ end
59
+ end
60
+ end
data/lib/verikloak-bff.rb CHANGED
@@ -18,3 +18,6 @@ require 'verikloak/bff/proxy_trust'
18
18
  require 'verikloak/bff/forwarded_token'
19
19
  require 'verikloak/bff/consistency_checks'
20
20
  require 'verikloak/bff/header_guard'
21
+ require 'verikloak/bff/rails'
22
+
23
+ require 'verikloak/bff/railtie' if defined?(Rails::Railtie)
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.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -55,20 +55,20 @@ dependencies:
55
55
  requirements:
56
56
  - - ">="
57
57
  - !ruby/object:Gem::Version
58
- version: 0.1.2
58
+ version: 0.2.0
59
59
  - - "<"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.2'
61
+ version: 1.0.0
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 0.1.2
68
+ version: 0.2.0
69
69
  - - "<"
70
70
  - !ruby/object:Gem::Version
71
- version: '0.2'
71
+ version: 1.0.0
72
72
  description: Framework-agnostic Rack middleware that normalizes forwarded tokens,
73
73
  enforces trust boundaries, and checks header/claims consistency before verikloak.
74
74
  executables: []
@@ -87,7 +87,10 @@ files:
87
87
  - lib/verikloak/bff/forwarded_token.rb
88
88
  - lib/verikloak/bff/header_guard.rb
89
89
  - lib/verikloak/bff/proxy_trust.rb
90
+ - lib/verikloak/bff/rails.rb
91
+ - lib/verikloak/bff/railtie.rb
90
92
  - lib/verikloak/bff/version.rb
93
+ - lib/verikloak/header_sources.rb
91
94
  homepage: https://github.com/taiyaky/verikloak-bff
92
95
  licenses:
93
96
  - MIT
@@ -95,7 +98,7 @@ metadata:
95
98
  source_code_uri: https://github.com/taiyaky/verikloak-bff
96
99
  changelog_uri: https://github.com/taiyaky/verikloak-bff/blob/main/CHANGELOG.md
97
100
  bug_tracker_uri: https://github.com/taiyaky/verikloak-bff/issues
98
- documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.1.2
101
+ documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.2.1
99
102
  rubygems_mfa_required: 'true'
100
103
  rdoc_options: []
101
104
  require_paths: