verikloak-bff 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3034b92c69f7147be681038b8f91a5a0c4ec1d577144a4372cad33b6645adb3a
4
+ data.tar.gz: a52b1ccc320fee7d5526f612fa509218a5b2232ce678d6d6e03a42d5c0122df0
5
+ SHA512:
6
+ metadata.gz: a9e31d538da66f06b1f18412b41520a3c1a5f0f5e3adf88af19f4aa68451420717002c11dc639a2f1dbc9441b64b68b74de2729d2ef8a5af805d862b7568dd75
7
+ data.tar.gz: cb2f118b465d7cb4a64f0aa9eb77cb81ec0bd5a82ae2b33c1e9992b1b518e5690dee84f7c5ae69189b83e0394de784d559c57daef41154c24f396390cb0d8bab
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [0.1.0] - 2025-09-14
11
+
12
+ ### Added
13
+ - Rack middleware `Verikloak::BFF::HeaderGuard`
14
+ - Bearer normalization (`ensure_bearer`), Authorization seeding (`token_header_priority`)
15
+ - Trust evaluation: REMOTE_ADDR first, XFF fallback (`peer_preference`, `xff_strategy`)
16
+ - Config keys: `forwarded_header_name`, `auth_request_headers`, `log_with`
17
+ - Claims/header consistency checks、`X-Auth-Request-*` stripping
18
+ - Env passthrough: `verikloak.bff.token`, `verikloak.bff.selected_peer`
19
+ - Docs: README、ERRORS、Rails guide (`docs/rails.md`)
20
+ - RSpec coverage for trust/consistency/seeding/env
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2025 taiyaky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # verikloak-bff
2
+
3
+ [![CI](https://github.com/taiyaky/verikloak-bff/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/taiyaky/verikloak-bff/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/verikloak-bff)](https://rubygems.org/gems/verikloak-bff)
5
+ ![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.1-blue)
6
+ [![Downloads](https://img.shields.io/gem/dt/verikloak-bff)](https://rubygems.org/gems/verikloak-bff)
7
+
8
+
9
+ A framework-agnostic Rack middleware that hardens the **BFF / reverse-auth** boundary (oauth2-proxy, Nginx `auth_request`, etc.). It **normalizes** access tokens from `X-Forwarded-Access-Token` into `Authorization` and optionally checks **consistency** between `X-Auth-Request-*` headers and JWT claims. Signature and `iss/aud/exp` validation are performed by the core `verikloak` middleware placed after this middleware.
10
+
11
+ ## Features
12
+ - Prefer / require `X-Forwarded-Access-Token` (configurable)
13
+ - Trust-boundary checking for proxy IPs (X-Forwarded-For parsing)
14
+ - Header consistency enforcement (Authorization vs Forwarded)
15
+ - Claims consistency checks (e.g., email/sub/groups) against `X-Auth-Request-*`
16
+ - Strip suspicious/forged headers before downstream
17
+ - Logging hooks for `request_id`, `sub`, `kid`, `iss/aud`
18
+
19
+ ## Installation
20
+ ```bash
21
+ bundle add verikloak-bff
22
+ ```
23
+
24
+ ## Usage
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)。
28
+
29
+ ## Consistency mapping
30
+
31
+ | Key | Header | JWT claim/path | Rule |
32
+ |----------|---------------------------|-------------------------|-------------|
33
+ | `email` | `X-Auth-Request-Email` | `email` | equality |
34
+ | `user` | `X-Auth-Request-User` | `sub` | equality |
35
+ | `groups` | `X-Auth-Request-Groups` | `realm_access.roles` | subset |
36
+
37
+ Use `enforce_claims_consistency: { email: :email, user: :sub, groups: :realm_roles }` to enable. Header names can be remapped via `auth_request_headers`.
38
+
39
+ See `examples/rack.ru` for a tiny Rack app demo.
40
+
41
+ ## Configuration
42
+
43
+ | Key | Type | Default | Description |
44
+ |----------------------------- |--------------------------------------|--------------|-------------|
45
+ | `trusted_proxies` | Array[String/Regexp/Proc] | `[]` | Allowlist for proxy peers (by IP/CIDR/regex/proc). |
46
+ | `prefer_forwarded` | Boolean | `true` | Prefer `X-Forwarded-Access-Token` over `Authorization`. |
47
+ | `require_forwarded_header` | Boolean | `false` | Reject when no `X-Forwarded-Access-Token` (blocks direct access). |
48
+ | `enforce_header_consistency` | Boolean | `true` | If both headers exist, require identical token values. |
49
+ | `enforce_claims_consistency` | Hash | `{}` | Mapping of header→claim to compare (e.g., `{ email: :email, user: :sub, groups: :realm_roles }`). |
50
+ | `strip_suspicious_headers` | Boolean | `true` | Remove external `X-Auth-Request-*` before passing downstream. |
51
+ | `xff_strategy` | Symbol (`:rightmost`/`:leftmost`) | `:rightmost` | Which peer to pick from `X-Forwarded-For`. |
52
+ | `peer_preference` | Symbol (`:remote_then_xff`/`:xff_only`) | `:remote_then_xff` | Whether to prefer `REMOTE_ADDR` before falling back to XFF. |
53
+ | `clock_skew_leeway` | Integer (seconds) | `30` | Reserved for small exp/nbf skew handled by core verifier. |
54
+ | `logger` | `Logger` or `nil` | `nil` | Logger for audit tags (`rid`, `sub`, `kid`, `iss/aud`). |
55
+ | `token_header_priority` | Array[String] | see code | 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. |
56
+ | `forwarded_header_name` | String | `HTTP_X_FORWARDED_ACCESS_TOKEN` | Env key for forwarded access token. |
57
+ | `auth_request_headers` | Hash | see code | Mapping for `X-Auth-Request-*` env keys: `{ email, user, groups }`. |
58
+
59
+ ## Errors
60
+ This gem returns concise, RFC 6750–style error responses with stable codes. See [ERRORS.md](ERRORS.md) for details and examples.
61
+
62
+ **Note:** This middleware does not verify JWT signatures or `iss/aud/exp` itself; it normalizes and guards headers so the core `verikloak` middleware always performs final verification.
63
+
64
+ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/rails.md](docs/rails.md).
65
+
66
+ ## Tips & Advanced Usage
67
+
68
+ - Peer preference: prefer `REMOTE_ADDR` before XFF
69
+ - 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
+ - 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
+ - Header name customization
73
+ - Forwarded-access-token header can be changed via:
74
+ - `forwarded_header_name: 'HTTP_X_CUSTOM_FORWARD_TOKEN'`
75
+ - X-Auth-Request-* header names can be remapped via:
76
+ - `auth_request_headers: { email: 'HTTP_X_EMAIL', user: 'HTTP_X_USER', groups: 'HTTP_X_GROUPS' }`
77
+
78
+ - Authorization seeding from priority headers
79
+ - When no token is chosen and `HTTP_AUTHORIZATION` is empty, the middleware consults `token_header_priority` to seed Authorization.
80
+ - `HTTP_AUTHORIZATION` itself is never used as a source; forwarded headers are considered only from trusted peers.
81
+
82
+ - Observability helpers
83
+ - 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`.
85
+ - 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
+
87
+ ## Development (for contributors)
88
+ Clone and install dependencies:
89
+
90
+ ```bash
91
+ git clone https://github.com/taiyaky/verikloak-bff.git
92
+ cd verikloak-bff
93
+ bundle install
94
+ ```
95
+ See **Testing** below to run specs and RuboCop. For releasing, see **Publishing**.
96
+
97
+ ## Testing
98
+ All pull requests and pushes are automatically tested with [RSpec](https://rspec.info/) and [RuboCop](https://rubocop.org/) via GitHub Actions.
99
+ See the CI badge at the top for current build status.
100
+
101
+ To run the test suite locally:
102
+
103
+ ```bash
104
+ docker compose run --rm dev rspec
105
+ docker compose run --rm dev rubocop -a
106
+ ```
107
+
108
+ ## Contributing
109
+ Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
110
+
111
+ ## Security
112
+ If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
113
+
114
+ ## License
115
+ This project is licensed under the [MIT License](LICENSE).
116
+
117
+ ## Publishing (for maintainers)
118
+ Gem release instructions are documented separately in [MAINTAINERS.md](MAINTAINERS.md).
119
+
120
+ ## Changelog
121
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
122
+
123
+ ## References
124
+ - Verikloak (core): https://github.com/taiyaky/verikloak
125
+ - verikloak-rails (Rails integration): https://github.com/taiyaky/verikloak-rails
126
+ - verikloak-bff on RubyGems: https://rubygems.org/gems/verikloak-bff
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configuration object for verikloak-bff. Holds middleware settings such as
4
+ # proxy trust rules, header consistency policies, and logging options.
5
+ #
6
+ # @!attribute [rw] trusted_proxies
7
+ # @return [Array<String, Regexp, Proc>] allowlist of trusted proxy peers
8
+ # @!attribute [rw] prefer_forwarded
9
+ # @return [Boolean] prefer X-Forwarded-Access-Token over Authorization
10
+ # @!attribute [rw] require_forwarded_header
11
+ # @return [Boolean] require X-Forwarded-Access-Token to be present
12
+ # @!attribute [rw] enforce_header_consistency
13
+ # @return [Boolean] when both headers exist, require equality
14
+ # @!attribute [rw] enforce_claims_consistency
15
+ # @return [Hash] mapping of header keys to claim keys (e.g., { email: :email })
16
+ # @!attribute [rw] strip_suspicious_headers
17
+ # @return [Boolean] strip X-Auth-Request-* headers before downstream
18
+ # @!attribute [rw] xff_strategy
19
+ # @return [Symbol] :rightmost or :leftmost peer selection from XFF
20
+ # @!attribute [rw] clock_skew_leeway
21
+ # @return [Integer] reserved leeway (seconds) for exp/nbf (handled in core)
22
+ # @!attribute [rw] logger
23
+ # @return [Logger, nil] optional logger for audit tags
24
+
25
+ module Verikloak
26
+ module BFF
27
+ # Configuration for Verikloak::BFF middleware (trusted proxies, header policies, logging, etc.).
28
+ class Configuration
29
+ attr_accessor :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
30
+ :enforce_header_consistency, :enforce_claims_consistency,
31
+ :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
+
35
+ # enforce_claims_consistency example:
36
+ # { email: :email, user: :sub, groups: :realm_roles }
37
+ def initialize
38
+ @trusted_proxies = []
39
+ @prefer_forwarded = true
40
+ @require_forwarded_header = false
41
+ @enforce_header_consistency = true
42
+ @enforce_claims_consistency = {}
43
+ @strip_suspicious_headers = true
44
+ @xff_strategy = :rightmost
45
+ @peer_preference = :remote_then_xff
46
+ @clock_skew_leeway = 30
47
+ @logger = nil
48
+ @log_with = nil
49
+ @forwarded_header_name = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
50
+ @auth_request_headers = {
51
+ email: 'HTTP_X_AUTH_REQUEST_EMAIL',
52
+ user: 'HTTP_X_AUTH_REQUEST_USER',
53
+ groups: 'HTTP_X_AUTH_REQUEST_GROUPS'
54
+ }
55
+ # When Authorization is empty and no chosen token exists, try these env headers (in order)
56
+ # to seed Authorization, similar to verikloak-rails behavior. HTTP_AUTHORIZATION is ignored as a source.
57
+ @token_header_priority = %w[
58
+ HTTP_X_FORWARDED_ACCESS_TOKEN
59
+ HTTP_AUTHORIZATION
60
+ ]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helpers to compare X-Auth-Request-* headers with JWT claims according to a
4
+ # provided mapping. Intended for lightweight, unverified JWT parsing only.
5
+ #
6
+ # @see .enforce!
7
+
8
+ require 'json'
9
+ require 'jwt' # used only to parse segments safely without verify
10
+
11
+ module Verikloak
12
+ module BFF
13
+ # Compares X-Auth-Request-* headers with JWT claims according to a mapping.
14
+ module ConsistencyChecks
15
+ module_function
16
+
17
+ # Parse JWT payload without verifying (verikloak core will verify later).
18
+ # Decode JWT payload without verification.
19
+ #
20
+ # @param token [String, nil]
21
+ # @return [Hash] claims or empty hash on error
22
+ def decode_claims(token)
23
+ return {} unless token
24
+
25
+ parts = token.split('.')
26
+ return {} unless parts.size >= 2
27
+
28
+ payload = JWT::Base64.url_decode(parts[1])
29
+ JSON.parse(payload)
30
+ rescue StandardError
31
+ {}
32
+ end
33
+
34
+ # mapping: { email: :email, user: :sub, groups: :realm_roles }
35
+ # Enforce consistency according to the provided mapping.
36
+ #
37
+ # @param env [Hash]
38
+ # @param token [String, nil]
39
+ # @param mapping [Hash] e.g., { email: :email, user: :sub, groups: :realm_roles }
40
+ # @return [true, Array(:error, Symbol)] true or error tuple with failing field
41
+ def enforce!(env, token, mapping, headers_map = nil)
42
+ return true if mapping.nil? || mapping.empty?
43
+
44
+ claims = decode_claims(token)
45
+
46
+ mapping.each do |header_key, claim_key|
47
+ hdr_val = extract_header_value(env, header_key, headers_map)
48
+ next if hdr_val.nil? # no header → skip comparison
49
+
50
+ case claim_key
51
+ when :realm_roles
52
+ roles = Array((claims['realm_access'] || {})['roles'] || [])
53
+ hdr_list = split_list(hdr_val)
54
+ return error(:groups) unless (hdr_list - roles).empty?
55
+ else
56
+ claim_val = dig_claim(claims, claim_key)
57
+ return error(header_key) unless claim_val && hdr_val.to_s == claim_val.to_s
58
+ end
59
+ end
60
+ true
61
+ end
62
+
63
+ # Extract an X-Auth-Request-* header value mapped from a symbolic key.
64
+ #
65
+ # @param env [Hash]
66
+ # @param key [Symbol]
67
+ # @return [String, nil]
68
+ def extract_header_value(env, key, headers_map = nil)
69
+ return env[headers_map[key]] if headers_map && headers_map[key]
70
+
71
+ case key
72
+ when :email
73
+ env['HTTP_X_AUTH_REQUEST_EMAIL']
74
+ when :user
75
+ env['HTTP_X_AUTH_REQUEST_USER']
76
+ when :groups
77
+ env['HTTP_X_AUTH_REQUEST_GROUPS']
78
+ end
79
+ end
80
+
81
+ # Split a comma-separated list into an array.
82
+ #
83
+ # @param val [String]
84
+ # @return [Array<String>]
85
+ def split_list(val)
86
+ Array(val.to_s.split(',').map(&:strip).reject(&:empty?))
87
+ end
88
+
89
+ # Dig into JWT claims by a symbol/string key or an array path.
90
+ #
91
+ # @param claims [Hash]
92
+ # @param key [Symbol, String, Array<String,Symbol>]
93
+ # @return [Object, nil]
94
+ def dig_claim(claims, key)
95
+ case key
96
+ when Symbol, String
97
+ claims[key.to_s]
98
+ when Array
99
+ key.reduce(claims) { |acc, k| acc.is_a?(Hash) ? acc[k.to_s] : nil }
100
+ end
101
+ end
102
+
103
+ # Build an error tuple for a failed field.
104
+ #
105
+ # @param field [Symbol]
106
+ # @return [Array(:error, Symbol)]
107
+ def error(field)
108
+ [:error, field]
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Error types emitted by verikloak-bff. These map to RFC6750-style responses
4
+ # with stable error codes for easy handling at clients and logs.
5
+
6
+ module Verikloak
7
+ module BFF
8
+ # Base error class with HTTP status and short code.
9
+ #
10
+ # @attr_reader [String] code
11
+ # @attr_reader [Integer] http_status
12
+ class Error < StandardError
13
+ attr_reader :code, :http_status
14
+
15
+ def initialize(message = nil, code: 'bff_error', http_status: 401)
16
+ super(message || code)
17
+ @code = code
18
+ @http_status = http_status
19
+ end
20
+ end
21
+
22
+ # Raised when a request did not pass through a trusted proxy peer.
23
+ class UntrustedProxyError < Error
24
+ def initialize(msg = 'request did not pass through a trusted proxy')
25
+ super(msg, code: 'untrusted_proxy', http_status: 401)
26
+ end
27
+ end
28
+
29
+ # Raised when require_forwarded_header is enabled but the forwarded token is absent.
30
+ class MissingForwardedTokenError < Error
31
+ def initialize(msg = 'missing X-Forwarded-Access-Token')
32
+ super(msg, code: 'missing_forwarded_token', http_status: 401)
33
+ end
34
+ end
35
+
36
+ # Raised when Authorization and X-Forwarded-Access-Token both exist and differ.
37
+ class HeaderMismatchError < Error
38
+ def initialize(msg = 'authorization and forwarded token mismatch')
39
+ super(msg, code: 'header_mismatch', http_status: 401)
40
+ end
41
+ end
42
+
43
+ # Raised when X-Auth-Request-* headers conflict with JWT claims.
44
+ class ClaimsMismatchError < Error
45
+ def initialize(field, msg = nil)
46
+ super(msg || "claims/header mismatch for #{field}", code: 'claims_mismatch', http_status: 403)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Utilities to extract, normalize, and manipulate forwarded/auth headers.
4
+ #
5
+ # @see .extract
6
+
7
+ module Verikloak
8
+ module BFF
9
+ # Helpers to extract and normalize forwarded/access token headers.
10
+ module ForwardedToken
11
+ module_function
12
+
13
+ FORWARDED_HEADER = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
14
+ AUTH_HEADER = 'HTTP_AUTHORIZATION'
15
+
16
+ # Extract normalized tokens from the Rack env.
17
+ #
18
+ # @param env [Hash]
19
+ # @return [Array(String, String)] [auth_token, forwarded_token]
20
+ def extract(env, forwarded_header_name = FORWARDED_HEADER, auth_header_name = AUTH_HEADER)
21
+ fwd_raw = env[forwarded_header_name]
22
+ auth_raw = env[auth_header_name]
23
+ [normalize_auth(auth_raw), normalize_forwarded(fwd_raw)]
24
+ end
25
+
26
+ # Only accept Bearer scheme for Authorization header.
27
+ #
28
+ # @param raw [String, nil]
29
+ # @return [String, nil] token or nil when not Bearer
30
+ def normalize_auth(raw)
31
+ return nil unless raw
32
+
33
+ token = raw.to_s.strip
34
+ return ::Regexp.last_match(1) if token =~ /^Bearer\s+(.+)$/i
35
+ return token[6..] if token =~ /^Bearer(?!\s)/i
36
+
37
+ nil
38
+ end
39
+
40
+ # Accept either bare token or Bearer for forwarded header.
41
+ #
42
+ # @param raw [String, nil]
43
+ # @return [String, nil] token or nil
44
+ def normalize_forwarded(raw)
45
+ return nil unless raw
46
+
47
+ token = raw.to_s.strip
48
+ return ::Regexp.last_match(1) if token =~ /^Bearer\s+(.+)$/i
49
+
50
+ token.empty? ? nil : token
51
+ end
52
+
53
+ # Normalize to a proper 'Bearer <token>' header value.
54
+ # - Detects scheme case-insensitively
55
+ # - Inserts a missing space (e.g., 'BearerXYZ' => 'Bearer XYZ')
56
+ # - Collapses multiple spaces/tabs after the scheme to a single space
57
+ # @param token [String]
58
+ # @return [String]
59
+ def ensure_bearer(token)
60
+ s = token.to_s.strip
61
+ # Case-insensitive 'Bearer' with spaces/tabs after
62
+ if s =~ /\ABearer[ \t]+/i
63
+ rest = s.sub(/\ABearer[ \t]+/i, '')
64
+ return "Bearer #{rest}"
65
+ end
66
+
67
+ # Case-insensitive 'Bearer' with no separator (e.g., 'BearerXYZ')
68
+ if s =~ /\ABearer(?![ \t])/i
69
+ rest = s[6..] || ''
70
+ return "Bearer #{rest}"
71
+ end
72
+
73
+ # No scheme present; add it
74
+ "Bearer #{s}"
75
+ end
76
+
77
+ # Set Authorization header to a normalized Bearer value (no overwrite when present).
78
+ #
79
+ # @param env [Hash]
80
+ # @param token [String]
81
+ def set_authorization!(env, token)
82
+ existing = env[AUTH_HEADER].to_s
83
+ # Overwrite only if Authorization is empty or not a valid Bearer value
84
+ return unless existing.empty? || normalize_auth(existing).nil?
85
+
86
+ env[AUTH_HEADER] = ensure_bearer(token)
87
+ end
88
+
89
+ # Remove potentially forged X-Auth-Request-* headers before passing
90
+ # downstream when not emitted by a trusted proxy.
91
+ #
92
+ # @param env [Hash]
93
+ def strip_suspicious!(env, headers = nil)
94
+ if headers.is_a?(Hash)
95
+ headers.each_value { |h| env.delete(h) }
96
+ return
97
+ end
98
+ env.delete('HTTP_X_AUTH_REQUEST_EMAIL')
99
+ env.delete('HTTP_X_AUTH_REQUEST_USER')
100
+ env.delete('HTTP_X_AUTH_REQUEST_GROUPS')
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rack middleware that hardens the BFF boundary by normalizing tokens and
4
+ # enforcing header/claims consistency before the core verifier runs.
5
+ #
6
+ # Typical usage (Rack/Rails):
7
+ # use Verikloak::BFF::HeaderGuard, trusted_proxies: ['127.0.0.1']
8
+ #
9
+ # The middleware prefers `X-Forwarded-Access-Token` (configurable), enforces
10
+ # header equality when both tokens exist, optionally checks `X-Auth-Request-*`
11
+ # headers against JWT claims, and normalizes the request into
12
+ # `HTTP_AUTHORIZATION: Bearer <token>` for the downstream verifier.
13
+ require 'rack'
14
+ require 'json'
15
+ require 'jwt'
16
+ require 'verikloak/bff/configuration'
17
+ require 'verikloak/bff/errors'
18
+ require 'verikloak/bff/proxy_trust'
19
+ require 'verikloak/bff/forwarded_token'
20
+ require 'verikloak/bff/consistency_checks'
21
+
22
+ module Verikloak
23
+ module BFF
24
+ # Rack middleware that enforces BFF boundary and header/claims consistency.
25
+ class HeaderGuard
26
+ # Accept both Rack 2 and Rack 3 builder call styles:
27
+ # - new(app, key: val)
28
+ # - new(app, { key: val })
29
+ #
30
+ # @param app [#call]
31
+ # @param opts [Hash, nil]
32
+ # @param opts_kw [Hash]
33
+ def initialize(app, opts = nil, **opts_kw)
34
+ @app = app
35
+ # Use a per-instance copy of global config to avoid cross-request/test side effects
36
+ @config = Verikloak::BFF.config.dup
37
+ combined = {}
38
+ combined.merge!(opts) if opts.is_a?(Hash)
39
+ combined.merge!(opts_kw) if opts_kw && !opts_kw.empty?
40
+ apply_overrides!(combined)
41
+ end
42
+
43
+ # Process a Rack request.
44
+ #
45
+ # @param env [Hash]
46
+ # @return [Array(Integer, Hash, Array<#to_s>)] Rack response triple
47
+ def call(env)
48
+ ensure_trusted_proxy!(env)
49
+ auth_token, fwd_token = ForwardedToken.extract(env, @config.forwarded_header_name)
50
+ ensure_forwarded_if_required!(fwd_token)
51
+ chosen = choose_token(auth_token, fwd_token)
52
+ chosen = seed_authorization_if_needed(env, chosen)
53
+
54
+ enforce_header_consistency!(env, auth_token, fwd_token)
55
+ enforce_claims_consistency!(env, chosen)
56
+ ForwardedToken.strip_suspicious!(env, @config.auth_request_headers) if @config.strip_suspicious_headers
57
+ normalize_authorization!(env, chosen, auth_token, fwd_token)
58
+ expose_env_hints(env, chosen)
59
+ @app.call(env)
60
+ rescue Verikloak::BFF::Error => e
61
+ respond_with_error(env, e)
62
+ end
63
+
64
+ private
65
+
66
+ # Apply per-instance configuration overrides.
67
+ #
68
+ # @param opts [Hash]
69
+ def apply_overrides!(opts)
70
+ cfg = @config
71
+ opts.each do |k, v|
72
+ cfg.public_send("#{k}=", v) if cfg.respond_to?("#{k}=")
73
+ end
74
+ end
75
+
76
+ # Choose a token based on preference and presence.
77
+ #
78
+ # @param auth_token [String, nil]
79
+ # @param fwd_token [String, nil]
80
+ # @return [String, nil]
81
+ def choose_token(auth_token, fwd_token)
82
+ return fwd_token if @config.prefer_forwarded && fwd_token
83
+
84
+ auth_token || fwd_token
85
+ end
86
+
87
+ # Describe the source of the chosen token for logging purposes.
88
+ #
89
+ # @param auth_token [String, nil]
90
+ # @param fwd_token [String, nil]
91
+ # @return [String] "authorization" or "forwarded"
92
+ def token_source(auth_token, fwd_token)
93
+ return 'forwarded' if @config.prefer_forwarded && fwd_token
94
+
95
+ if auth_token && fwd_token
96
+ 'authorization'
97
+ else
98
+ (auth_token ? 'authorization' : 'forwarded')
99
+ end
100
+ end
101
+
102
+ # Extract selected JWT tags for audit logging (best-effort, unverified).
103
+ #
104
+ # @param token [String, nil]
105
+ # @return [Hash] subset of {sub, iss, aud, kid}
106
+ def token_tags(token)
107
+ return {} unless token
108
+
109
+ payload, header = decode_unverified(token)
110
+ {
111
+ sub: payload['sub'],
112
+ iss: payload['iss'],
113
+ aud: (payload['aud'].is_a?(Array) ? payload['aud'].join(' ') : payload['aud']).to_s,
114
+ kid: header['kid']
115
+ }.compact
116
+ rescue StandardError
117
+ {}
118
+ end
119
+
120
+ # Decode JWT header/payload without validation (for logging only).
121
+ #
122
+ # @param token [String]
123
+ # @return [Array(Hash, Hash)] [payload, header]
124
+ def decode_unverified(token)
125
+ parts = token.to_s.split('.')
126
+ return [{}, {}] unless parts.size >= 2
127
+
128
+ payload = begin
129
+ JSON.parse(::JWT::Base64.url_decode(parts[1]))
130
+ rescue StandardError
131
+ {}
132
+ end
133
+ header = begin
134
+ JSON.parse(::JWT::Base64.url_decode(parts[0]))
135
+ rescue StandardError
136
+ {}
137
+ end
138
+ [payload, header]
139
+ end
140
+
141
+ # Extract request id for logging from common headers.
142
+ #
143
+ # @param env [Hash]
144
+ # @return [String, nil]
145
+ def request_id(env)
146
+ env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
147
+ end
148
+
149
+ # Resolve logger to use (config-provided or rack.logger).
150
+ #
151
+ # @param env [Hash]
152
+ # @return [Logger, nil]
153
+ def logger(env)
154
+ @config.logger || env['rack.logger']
155
+ end
156
+
157
+ # Emit a structured log line if a logger is present.
158
+ #
159
+ # @param env [Hash]
160
+ # @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
161
+ # @param attrs [Hash]
162
+ def log_event(env, kind, **attrs)
163
+ lg = logger(env)
164
+ payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
165
+ if @config.log_with.respond_to?(:call)
166
+ begin
167
+ @config.log_with.call(payload)
168
+ rescue StandardError
169
+ # ignore log hook failures
170
+ end
171
+ end
172
+ return unless lg
173
+
174
+ msg = payload.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
175
+ level = (kind == :ok ? :info : :warn)
176
+ lg.public_send(level, msg)
177
+ rescue StandardError
178
+ # no-op on logging errors
179
+ end
180
+
181
+ # Raise when the request did not come through a trusted proxy.
182
+ #
183
+ # @param env [Hash]
184
+ def ensure_trusted_proxy!(env)
185
+ return if ProxyTrust.trusted?(env, @config.trusted_proxies, @config.xff_strategy)
186
+
187
+ raise UntrustedProxyError
188
+ end
189
+
190
+ # Enforce presence of forwarded token when required.
191
+ #
192
+ # @param fwd_token [String, nil]
193
+ def ensure_forwarded_if_required!(fwd_token)
194
+ return unless @config.require_forwarded_header
195
+ raise MissingForwardedTokenError if fwd_token.nil? || fwd_token.to_s.strip.empty?
196
+ end
197
+
198
+ # Enforce equality when both Authorization and Forwarded tokens exist.
199
+ #
200
+ # @param env [Hash]
201
+ # @param auth_token [String, nil]
202
+ # @param fwd_token [String, nil]
203
+ def enforce_header_consistency!(env, auth_token, fwd_token)
204
+ return unless @config.enforce_header_consistency
205
+ return unless auth_token && fwd_token
206
+ return if auth_token == fwd_token
207
+
208
+ log_event(env, :mismatch, reason: 'authorization_vs_forwarded')
209
+ raise HeaderMismatchError
210
+ end
211
+
212
+ # Enforce X-Auth-Request-* ↔ JWT claims mapping when configured.
213
+ #
214
+ # @param env [Hash]
215
+ # @param chosen [String, nil]
216
+ def enforce_claims_consistency!(env, chosen)
217
+ res = ConsistencyChecks.enforce!(env, chosen, @config.enforce_claims_consistency, @config.auth_request_headers)
218
+ return unless res.is_a?(Array) && res.first == :error
219
+
220
+ field = res.last
221
+ log_event(env, :claims_mismatch, field: field.to_s)
222
+ raise ClaimsMismatchError, field
223
+ end
224
+
225
+ # Set normalized Authorization header and emit success log.
226
+ #
227
+ # @param env [Hash]
228
+ # @param chosen [String, nil]
229
+ # @param auth_token [String, nil]
230
+ # @param fwd_token [String, nil]
231
+ def normalize_authorization!(env, chosen, auth_token, fwd_token)
232
+ return unless chosen
233
+
234
+ ForwardedToken.set_authorization!(env, chosen)
235
+ log_event(env, :ok, source: token_source(auth_token, fwd_token), **token_tags(chosen))
236
+ end
237
+
238
+ # Build a minimal RFC6750-style error response.
239
+ #
240
+ # @param env [Hash]
241
+ # @param error [Verikloak::BFF::Error]
242
+ # @return [Array(Integer, Hash, Array<String>)]
243
+ def respond_with_error(env, error)
244
+ log_event(env, :error, code: error.code)
245
+ body = { error: error.code, message: error.message }.to_json
246
+ headers = { 'Content-Type' => 'application/json',
247
+ 'WWW-Authenticate' => %(Bearer error="#{error.code}", error_description="#{error.message}") }
248
+ [error.http_status, headers, [body]]
249
+ end
250
+
251
+ # Resolve the first env header from which to source a bearer token.
252
+ # Forwarded is considered only when the peer is trusted; HTTP_AUTHORIZATION is never a source.
253
+ # @param env [Hash]
254
+ # @return [String, nil]
255
+ def resolve_first_token_header(env)
256
+ candidates = Array(@config.token_header_priority).dup
257
+ candidates -= ['HTTP_AUTHORIZATION']
258
+ fwd_key = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
259
+ if candidates.include?(fwd_key) && !ProxyTrust.from_trusted_proxy?(env, @config.trusted_proxies)
260
+ candidates -= [fwd_key]
261
+ end
262
+ candidates.find { |k| (v = env[k]) && !v.to_s.empty? }
263
+ end
264
+
265
+ # Seed Authorization from priority headers if nothing chosen and empty Authorization
266
+ # @param env [Hash]
267
+ # @param chosen [String, nil]
268
+ # @return [String, nil] possibly updated chosen token
269
+ def seed_authorization_if_needed(env, chosen)
270
+ return chosen unless chosen.nil? && env['HTTP_AUTHORIZATION'].to_s.empty?
271
+ return chosen unless Array(@config.token_header_priority).any?
272
+
273
+ seeded = resolve_first_token_header(env)
274
+ if seeded
275
+ ForwardedToken.set_authorization!(env, env[seeded])
276
+ return ForwardedToken.normalize_forwarded(env[seeded]) || env[seeded].to_s
277
+ end
278
+ chosen
279
+ end
280
+
281
+ # Expose hints to downstream
282
+ # @param env [Hash]
283
+ # @param chosen [String, nil]
284
+ def expose_env_hints(env, chosen)
285
+ env['verikloak.bff.token'] = chosen if chosen
286
+ env['verikloak.bff.selected_peer'] =
287
+ ProxyTrust.selected_peer(env, @config.peer_preference, @config.xff_strategy)
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Utilities to determine whether a request peer (via REMOTE_ADDR / XFF) is
4
+ # allow‑listed as a trusted proxy.
5
+ require 'ipaddr'
6
+
7
+ module Verikloak
8
+ module BFF
9
+ # Determines whether the selected peer (via XFF/REMOTE_ADDR) is a trusted proxy.
10
+ module ProxyTrust
11
+ module_function
12
+
13
+ # Determine if the immediate peer (based on REMOTE_ADDR / X-Forwarded-For) is trusted.
14
+ # strategy :rightmost (typical when proxy appends client IP to the right)
15
+ #
16
+ # @param env [Hash] Rack environment
17
+ # @param trusted [Array<String, Regexp, Proc>, nil] Allowlist of proxy peers.
18
+ # - String: exact IP (e.g. "127.0.0.1") or CIDR (e.g. "10.0.0.0/8")
19
+ # - Regexp: matched against the selected peer IP
20
+ # - Proc: called as `->(ip, env) { ... }` and returns truthy when trusted
21
+ # @param strategy [Symbol, String] `:rightmost` (default) or `:leftmost` for XFF parsing
22
+ # @return [Boolean] true if the selected peer is trusted
23
+ # @example CIDR + Regex allowlist
24
+ # ProxyTrust.trusted?(env, ["10.0.0.0/8", /^192\.168\./], :rightmost)
25
+ def trusted?(env, trusted, strategy = :rightmost)
26
+ return true if trusted.nil? || trusted.empty?
27
+
28
+ # Rails-aligned: prefer REMOTE_ADDR; fallback to nearest XFF entry
29
+ remote = (env['REMOTE_ADDR'] || '').to_s.strip
30
+ remote = extract_peer_ip(env, strategy) if remote.empty?
31
+ return false unless remote
32
+
33
+ remote_ip = ip_or_nil(remote)
34
+ trusted.any? { |rule| rule_trusts?(rule, remote, remote_ip, env) }
35
+ rescue StandardError
36
+ false
37
+ end
38
+
39
+ # Select the peer IP from X-Forwarded-For according to strategy or fall back to REMOTE_ADDR.
40
+ #
41
+ # @param env [Hash] Rack environment
42
+ # @param strategy [Symbol, String] `:rightmost` (default) or `:leftmost`
43
+ # @return [String, nil] Selected peer IP, or nil if not determinable
44
+ def extract_peer_ip(env, strategy)
45
+ mode = strategy.to_s.to_sym
46
+ xff = env['HTTP_X_FORWARDED_FOR']
47
+ if xff && !xff.strip.empty?
48
+ parts = xff.split(',').map(&:strip)
49
+ ip = mode == :leftmost ? parts.first : parts.last
50
+ return ip
51
+ end
52
+ env['REMOTE_ADDR']
53
+ end
54
+
55
+ # Return the selected peer IP according to preference and strategy.
56
+ # @param env [Hash]
57
+ # @param preference [Symbol] :remote_then_xff or :xff_only
58
+ # @param strategy [Symbol] :rightmost or :leftmost
59
+ # @return [String, nil]
60
+ def selected_peer(env, preference, strategy)
61
+ case preference.to_s.to_sym
62
+ when :remote_then_xff
63
+ ip = (env['REMOTE_ADDR'] || '').to_s.strip
64
+ return ip unless ip.nil? || ip.empty?
65
+
66
+ extract_peer_ip(env, :rightmost) # nearest by default
67
+ else
68
+ extract_peer_ip(env, strategy)
69
+ end
70
+ end
71
+
72
+ # Parse string to IPAddr or nil on failure.
73
+ # @param str [String]
74
+ # @return [IPAddr, nil]
75
+ def ip_or_nil(str)
76
+ IPAddr.new(str)
77
+ rescue StandardError
78
+ nil
79
+ end
80
+
81
+ # Check whether a single rule trusts the selected remote.
82
+ # @param rule [String, Regexp, Proc]
83
+ # @param remote [String]
84
+ # @param remote_ip [IPAddr, nil]
85
+ # @param env [Hash]
86
+ # @return [Boolean]
87
+ def rule_trusts?(rule, remote, remote_ip, env)
88
+ case rule
89
+ when String
90
+ if rule.include?('/') # CIDR
91
+ cidr = IPAddr.new(rule)
92
+ remote_ip ? cidr.include?(remote_ip) : false
93
+ else
94
+ remote == rule
95
+ end
96
+ when Regexp
97
+ remote =~ rule
98
+ when Proc
99
+ rule.call(remote, env)
100
+ else
101
+ false
102
+ end
103
+ end
104
+
105
+ # Determine if the request originates from a trusted proxy subnet.
106
+ # Rails-aligned behavior: prefer REMOTE_ADDR, fallback to nearest (rightmost) X-Forwarded-For.
107
+ # @param env [Hash]
108
+ # @param trusted [Array<String, Regexp, Proc>, nil]
109
+ # @return [Boolean]
110
+ def self.from_trusted_proxy?(env, trusted)
111
+ return true if trusted.nil? || trusted.empty?
112
+
113
+ ip = (env['REMOTE_ADDR'] || '').to_s.strip
114
+ ip = env['HTTP_X_FORWARDED_FOR'].to_s.split(',').last.to_s.strip if ip.empty? && env['HTTP_X_FORWARDED_FOR']
115
+ return false if ip.empty?
116
+
117
+ remote_ip = ip_or_nil(ip)
118
+ return false unless remote_ip
119
+
120
+ trusted.any? { |rule| rule_trusts?(rule, ip, remote_ip, env) }
121
+ rescue StandardError
122
+ false
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version constant for verikloak-bff.
4
+ #
5
+ # @return [String]
6
+ module Verikloak
7
+ module BFF
8
+ VERSION = '0.1.0'
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Namespace for BFF-related configuration and helpers used by verikloak-bff.
4
+ #
5
+ # @see Verikloak::BFF::Configuration
6
+
7
+ module Verikloak
8
+ # Top-level namespace for verikloak-bff features and configuration.
9
+ module BFF
10
+ class << self
11
+ # Configure global settings for the BFF middleware.
12
+ #
13
+ # @yieldparam config [Verikloak::BFF::Configuration]
14
+ # @return [Verikloak::BFF::Configuration]
15
+ def configure
16
+ @config ||= Configuration.new
17
+ yield @config if block_given?
18
+ @config
19
+ end
20
+
21
+ # Current global configuration.
22
+ #
23
+ # @return [Verikloak::BFF::Configuration]
24
+ def config
25
+ @config ||= Configuration.new
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # verikloak-bff — library entrypoint.
4
+ #
5
+ # Requiring this file loads the BFF namespace, configuration, middleware, and
6
+ # supporting utilities. Applications typically need only:
7
+ #
8
+ # require 'verikloak-bff'
9
+ # use Verikloak::BFF::HeaderGuard, trusted_proxies: ['127.0.0.1']
10
+ #
11
+ # @see Verikloak::BFF::HeaderGuard
12
+ # @see Verikloak::BFF::Configuration
13
+ require 'verikloak/bff'
14
+ require 'verikloak/bff/version'
15
+ require 'verikloak/bff/configuration'
16
+ require 'verikloak/bff/errors'
17
+ require 'verikloak/bff/proxy_trust'
18
+ require 'verikloak/bff/forwarded_token'
19
+ require 'verikloak/bff/consistency_checks'
20
+ require 'verikloak/bff/header_guard'
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: verikloak-bff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - taiyaky
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jwt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.7'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rack
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.2'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '4.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '2.2'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '4.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: verikloak
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 0.1.2
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.2'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 0.1.2
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '0.2'
66
+ description: Framework-agnostic Rack middleware that normalizes forwarded tokens,
67
+ enforces trust boundaries, and checks header/claims consistency before verikloak.
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - CHANGELOG.md
73
+ - LICENSE
74
+ - README.md
75
+ - lib/verikloak-bff.rb
76
+ - lib/verikloak/bff.rb
77
+ - lib/verikloak/bff/configuration.rb
78
+ - lib/verikloak/bff/consistency_checks.rb
79
+ - lib/verikloak/bff/errors.rb
80
+ - lib/verikloak/bff/forwarded_token.rb
81
+ - lib/verikloak/bff/header_guard.rb
82
+ - lib/verikloak/bff/proxy_trust.rb
83
+ - lib/verikloak/bff/version.rb
84
+ homepage: https://github.com/taiyaky/verikloak-bff
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ source_code_uri: https://github.com/taiyaky/verikloak-bff
89
+ changelog_uri: https://github.com/taiyaky/verikloak-bff/blob/main/CHANGELOG.md
90
+ bug_tracker_uri: https://github.com/taiyaky/verikloak-bff/issues
91
+ documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.1.0
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '3.1'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.6.9
108
+ specification_version: 4
109
+ summary: BFF header guard for verikloak (oauth2-proxy / auth_request integration)
110
+ test_files: []