verikloak-bff 0.2.5 → 0.3.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 +30 -0
- data/README.md +115 -3
- data/lib/generators/verikloak/bff/install/templates/initializer.rb.erb +126 -8
- data/lib/verikloak/bff/configuration.rb +4 -1
- data/lib/verikloak/bff/header_guard.rb +104 -76
- data/lib/verikloak/bff/version.rb +1 -1
- 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: 8a79baf28ee05a8774b36916c71281bd72cd35825148413c15e9f865b85416cb
|
|
4
|
+
data.tar.gz: 739244544590c0f306eff4d587c27c312adc4c081646a45d2ec4a58e83ce6abb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 713b4e27ee284e38917a6fd4880414cdb8792fbffd4f7713112d2fa619f5ecf3120379a4a0b29eb115ce075c6373e0a4b25f1bca38bd8e8af0ff683944dcd8fd
|
|
7
|
+
data.tar.gz: ed258207b16fdb79fb079656393e0f7fd4bc1a650d942c6c19060c0aeaf45f8e63fa393a115bb8f80ea87f9c0d84cd025bb55a5ffb5259a981007211c896b786
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2025-01-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`disabled` configuration option**: Explicitly disable the middleware in pass-through mode. When `disabled: false` (default) and `trusted_proxies` is not configured, a `ConfigurationError` is raised at startup.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **BREAKING**: `trusted_proxies` is now **required** when `disabled: false`. Previously, an empty `trusted_proxies` would silently disable the middleware (fail-open). Now it raises `Verikloak::BFF::HeaderGuard::ConfigurationError` to prevent unintended security gaps.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Security**: Prevent fail-open vulnerability where unset `trusted_proxies` could silently bypass proxy trust validation.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [0.2.6] - 2025-12-31
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Rails 8.x+ compatibility**: Remove `after_initialize` middleware insertion from generator template to avoid `FrozenError` when middleware stack is frozen.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Generator (`rails g verikloak:bff:install`) now creates a **configuration-only** initializer. Middleware insertion is handled automatically by `verikloak-rails`.
|
|
30
|
+
- Generated initializer includes comprehensive configuration options with documentation comments.
|
|
31
|
+
- **Breaking**: Minimum `verikloak` dependency raised from `>= 0.2.0` to `>= 0.3.0`.
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
- Add "Rails Integration" section explaining automatic middleware detection with `verikloak-rails`.
|
|
35
|
+
- Add warning about Rails 8.x+ middleware stack freeze in `after_initialize`.
|
|
36
|
+
- Add "oauth2-proxy Integration" section with header configuration reference and recommended settings.
|
|
37
|
+
- Document manual middleware setup option for users not using `verikloak-rails`.
|
|
38
|
+
- Update `docs/rails.md` with clearer setup instructions and Rails 8.x support note.
|
|
39
|
+
|
|
10
40
|
## [0.2.5] - 2025-09-28
|
|
11
41
|
|
|
12
42
|
### Changed
|
data/README.md
CHANGED
|
@@ -31,16 +31,25 @@ use Verikloak::BFF::HeaderGuard, trusted_proxies: ['127.0.0.1', '10.0.0.0/8']
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### Rails Applications
|
|
34
|
-
Add
|
|
34
|
+
Add both gems to your Gemfile:
|
|
35
35
|
```ruby
|
|
36
|
+
gem 'verikloak-rails'
|
|
36
37
|
gem 'verikloak-bff'
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
Run the install generator to create a configuration file:
|
|
39
41
|
```sh
|
|
40
42
|
bin/rails g verikloak:bff:install
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
The
|
|
45
|
+
The generator creates `config/initializers/verikloak_bff.rb` with BFF configuration options. **Edit this file to set `trusted_proxies`** (required unless `disabled: true`) and customize other options as needed.
|
|
46
|
+
|
|
47
|
+
**Note**: Middleware insertion is handled automatically by `verikloak-rails`. The generated initializer only contains configuration—no manual middleware setup is required.
|
|
48
|
+
|
|
49
|
+
> **Without verikloak-rails**: You can also configure middleware manually in `config/application.rb`:
|
|
50
|
+
> ```ruby
|
|
51
|
+
> config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
|
|
52
|
+
> ```
|
|
44
53
|
|
|
45
54
|
For detailed configuration, proxy setup examples, and troubleshooting, see [docs/rails.md](docs/rails.md).
|
|
46
55
|
|
|
@@ -60,7 +69,8 @@ See `examples/rack.ru` for a tiny Rack app demo.
|
|
|
60
69
|
|
|
61
70
|
| Key | Type | Default | Description |
|
|
62
71
|
|----------------------------- |--------------------------------------|--------------|-------------|
|
|
63
|
-
| `
|
|
72
|
+
| `disabled` | Boolean | `false` | Explicitly disable the middleware (pass-through mode). When `false`, `trusted_proxies` must be configured. |
|
|
73
|
+
| `trusted_proxies` | Array[String/Regexp/Proc] | *(required)* | Allowlist for proxy peers (by IP/CIDR/regex/proc). **Required** unless `disabled: true`. |
|
|
64
74
|
| `prefer_forwarded` | Boolean | `true` | Prefer `X-Forwarded-Access-Token` over `Authorization`. |
|
|
65
75
|
| `require_forwarded_header` | Boolean | `false` | Reject when no `X-Forwarded-Access-Token` (blocks direct access). |
|
|
66
76
|
| `enforce_header_consistency` | Boolean | `true` | If both headers exist, require identical token values. |
|
|
@@ -75,6 +85,19 @@ See `examples/rack.ru` for a tiny Rack app demo.
|
|
|
75
85
|
| `forwarded_header_name` | String | `HTTP_X_FORWARDED_ACCESS_TOKEN` | Env key for forwarded access token. |
|
|
76
86
|
| `auth_request_headers` | Hash | see code | Mapping for `X-Auth-Request-*` env keys: `{ email, user, groups }`. |
|
|
77
87
|
|
|
88
|
+
### Configuration Requirements
|
|
89
|
+
|
|
90
|
+
When `disabled: false` (the default), you **must** configure `trusted_proxies`. If it is not set, the middleware will raise a `ConfigurationError` at startup to prevent a fail-open security vulnerability.
|
|
91
|
+
|
|
92
|
+
To explicitly disable the middleware (e.g., for development or testing), set `disabled: true`:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# Pass-through mode: no proxy trust checking
|
|
96
|
+
Verikloak::BFF.configure do |config|
|
|
97
|
+
config.disabled = true
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
78
101
|
## Errors
|
|
79
102
|
This gem returns concise, RFC 6750–style error responses with stable codes. See [ERRORS.md](ERRORS.md) for details and examples.
|
|
80
103
|
|
|
@@ -110,6 +133,95 @@ For full reverse proxy examples (Nginx auth_request / oauth2-proxy), see [docs/r
|
|
|
110
133
|
- Claims consistency modes
|
|
111
134
|
- Default `:enforce` mode rejects requests with mismatches. Switch to `claims_consistency_mode: :log_only` when you only need observability signals; downstream services must continue verifying JWT signatures, issuer, audience, and expirations.
|
|
112
135
|
|
|
136
|
+
## Rails Integration
|
|
137
|
+
|
|
138
|
+
### Recommended: Use with verikloak-rails (Auto-detection)
|
|
139
|
+
|
|
140
|
+
When both `verikloak-rails` and `verikloak-bff` are installed, the railtie automatically inserts the BFF middleware. No manual configuration is required.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# Gemfile
|
|
144
|
+
gem 'verikloak-rails'
|
|
145
|
+
gem 'verikloak-bff'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The middleware stack will be configured as:
|
|
149
|
+
```
|
|
150
|
+
[Verikloak::BFF::HeaderGuard] → [Verikloak::Middleware] → [Your App]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### ⚠️ Rails 8.x+ Middleware Stack Freeze
|
|
154
|
+
|
|
155
|
+
In Rails 8.x and later, the middleware stack is frozen after the `after_initialize` callback. Manual middleware insertion in `after_initialize` will raise `FrozenError`:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# ❌ This will NOT work in Rails 8.x+
|
|
159
|
+
Rails.application.config.after_initialize do
|
|
160
|
+
Rails.application.config.middleware.insert_after(
|
|
161
|
+
Verikloak::Middleware,
|
|
162
|
+
Verikloak::BFF::HeaderGuard
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
# => FrozenError: can't modify frozen Array
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Solution**: Use the automatic detection feature with `verikloak-rails`, or insert middleware in `config/application.rb` or an initializer (which runs before the stack is frozen):
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# ✅ This works - config/application.rb
|
|
172
|
+
module MyApp
|
|
173
|
+
class Application < Rails::Application
|
|
174
|
+
config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## oauth2-proxy Integration
|
|
180
|
+
|
|
181
|
+
### Header Configuration Reference
|
|
182
|
+
|
|
183
|
+
| oauth2-proxy Setting | Header Sent | HeaderGuard Behavior |
|
|
184
|
+
|---------------------|-------------|---------------------|
|
|
185
|
+
| `--pass-access-token=true` | Cookie (`_oauth2_proxy`) | Not supported (cookie-based) |
|
|
186
|
+
| `--pass-access-token=true` + nginx | `X-Forwarded-Access-Token` | Normalized to `Authorization` (default) |
|
|
187
|
+
| `--set-authorization-header=true` | `Authorization: Bearer <token>` | Used directly |
|
|
188
|
+
| `--set-xauthrequest=true` | `X-Auth-Request-Access-Token` | Requires `token_header_priority` config |
|
|
189
|
+
|
|
190
|
+
### Recommended oauth2-proxy Configuration
|
|
191
|
+
|
|
192
|
+
For best compatibility, configure oauth2-proxy to emit `X-Forwarded-Access-Token` via nginx:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
oauth2-proxy \
|
|
196
|
+
--pass-access-token=true \
|
|
197
|
+
--set-authorization-header=false \
|
|
198
|
+
--set-xauthrequest=true \
|
|
199
|
+
--upstream=http://rails-api:3000
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then configure nginx to relay the token:
|
|
203
|
+
|
|
204
|
+
```nginx
|
|
205
|
+
proxy_set_header X-Forwarded-Access-Token $upstream_http_x_auth_request_access_token;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Using X-Auth-Request-Access-Token Directly
|
|
209
|
+
|
|
210
|
+
If you prefer to use `X-Auth-Request-Access-Token` without nginx relay, configure both `forwarded_header_name` and `token_header_priority`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
use Verikloak::BFF::HeaderGuard,
|
|
214
|
+
trusted_proxies: ['10.0.0.0/8'],
|
|
215
|
+
# Set the forwarded header to X-Auth-Request-Access-Token
|
|
216
|
+
forwarded_header_name: 'HTTP_X_AUTH_REQUEST_ACCESS_TOKEN',
|
|
217
|
+
# Also add to priority list for Authorization seeding
|
|
218
|
+
token_header_priority: ['HTTP_X_AUTH_REQUEST_ACCESS_TOKEN']
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
> **Note**: If you only set `token_header_priority` without changing `forwarded_header_name`,
|
|
222
|
+
> `require_forwarded_header: true` will still expect `X-Forwarded-Access-Token` and return 401
|
|
223
|
+
> when it's missing.
|
|
224
|
+
|
|
113
225
|
## Development (for contributors)
|
|
114
226
|
Clone and install dependencies:
|
|
115
227
|
|
|
@@ -1,11 +1,129 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
# Verikloak BFF Configuration
|
|
4
|
+
#
|
|
5
|
+
# This file configures the Verikloak::BFF::HeaderGuard middleware.
|
|
6
|
+
#
|
|
7
|
+
# IMPORTANT: Middleware insertion is handled automatically by verikloak-rails.
|
|
8
|
+
# Make sure you have both gems in your Gemfile:
|
|
9
|
+
#
|
|
10
|
+
# gem 'verikloak-rails'
|
|
11
|
+
# gem 'verikloak-bff'
|
|
12
|
+
#
|
|
13
|
+
# The middleware stack will be configured as:
|
|
14
|
+
# [Verikloak::BFF::HeaderGuard] → [Verikloak::Middleware] → [Your App]
|
|
15
|
+
#
|
|
16
|
+
# If you are NOT using verikloak-rails, you must manually insert the middleware
|
|
17
|
+
# in config/application.rb (NOT in after_initialize, which causes FrozenError in Rails 8.x+):
|
|
18
|
+
#
|
|
19
|
+
# config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
|
|
20
|
+
#
|
|
21
|
+
Verikloak::BFF.configure do |config|
|
|
22
|
+
# ==========================================================================
|
|
23
|
+
# Disable Middleware (Development/Testing Only)
|
|
24
|
+
# ==========================================================================
|
|
25
|
+
# Explicitly disable the middleware (pass-through mode).
|
|
26
|
+
# When disabled: false (default), trusted_proxies MUST be configured.
|
|
27
|
+
#
|
|
28
|
+
# SECURITY WARNING: Only set to true in development/test environments.
|
|
29
|
+
# Production deployments should ALWAYS configure trusted_proxies.
|
|
30
|
+
#
|
|
31
|
+
# config.disabled = ENV.fetch('VERIKLOAK_BFF_DISABLED', 'false') == 'true'
|
|
32
|
+
|
|
33
|
+
# ==========================================================================
|
|
34
|
+
# Trust Boundary (REQUIRED when disabled: false)
|
|
35
|
+
# ==========================================================================
|
|
36
|
+
# Allowlist for trusted proxy peers. Requests from untrusted peers are rejected.
|
|
37
|
+
# Supports IP addresses, CIDR ranges, Regexp, or Proc.
|
|
38
|
+
#
|
|
39
|
+
# SECURITY: Keep this list as specific as possible. Review whenever proxy
|
|
40
|
+
# topology changes to avoid unintentionally widening the trust boundary.
|
|
41
|
+
#
|
|
42
|
+
# NOTE: This setting is REQUIRED. If not configured and disabled is false,
|
|
43
|
+
# a ConfigurationError will be raised at startup to prevent fail-open vulnerabilities.
|
|
44
|
+
#
|
|
45
|
+
config.trusted_proxies = ENV.fetch('TRUSTED_PROXIES', '127.0.0.1').split(',').map(&:strip)
|
|
46
|
+
|
|
47
|
+
# ==========================================================================
|
|
48
|
+
# Peer Selection
|
|
49
|
+
# ==========================================================================
|
|
50
|
+
# How to determine the client's peer IP for trust decisions.
|
|
51
|
+
#
|
|
52
|
+
# :remote_then_xff (default) - Prefer REMOTE_ADDR, then fall back to XFF
|
|
53
|
+
# :xff_only - Use only X-Forwarded-For header
|
|
54
|
+
#
|
|
55
|
+
# config.peer_preference = :remote_then_xff
|
|
56
|
+
|
|
57
|
+
# Which X-Forwarded-For entry to use when multiple proxies are involved.
|
|
58
|
+
#
|
|
59
|
+
# :rightmost (default) - Nearest proxy (recommended for most setups)
|
|
60
|
+
# :leftmost - Original client (use only if you trust all proxies)
|
|
61
|
+
#
|
|
62
|
+
# config.xff_strategy = :rightmost
|
|
63
|
+
|
|
64
|
+
# ==========================================================================
|
|
65
|
+
# Token Selection & Header Handling
|
|
66
|
+
# ==========================================================================
|
|
67
|
+
# Prefer X-Forwarded-Access-Token over Authorization header.
|
|
68
|
+
#
|
|
69
|
+
# config.prefer_forwarded = true
|
|
70
|
+
|
|
71
|
+
# Reject requests without X-Forwarded-Access-Token (blocks direct access).
|
|
72
|
+
# Enable this to ensure all requests go through the BFF proxy.
|
|
73
|
+
#
|
|
74
|
+
# config.require_forwarded_header = false
|
|
75
|
+
|
|
76
|
+
# Require Authorization and X-Forwarded-Access-Token to contain the same token
|
|
77
|
+
# when both are present.
|
|
78
|
+
#
|
|
79
|
+
# config.enforce_header_consistency = true
|
|
80
|
+
|
|
81
|
+
# Remove external X-Auth-Request-* headers before passing downstream.
|
|
82
|
+
# Prevents header injection attacks.
|
|
83
|
+
#
|
|
84
|
+
# config.strip_suspicious_headers = true
|
|
85
|
+
|
|
86
|
+
# ==========================================================================
|
|
87
|
+
# Claims Consistency (Optional)
|
|
88
|
+
# ==========================================================================
|
|
89
|
+
# Compare X-Auth-Request-* headers against JWT claims.
|
|
90
|
+
# Useful for detecting token substitution attacks.
|
|
91
|
+
#
|
|
92
|
+
# config.enforce_claims_consistency = {
|
|
93
|
+
# email: :email, # X-Auth-Request-Email == JWT email claim
|
|
94
|
+
# user: :sub, # X-Auth-Request-User == JWT sub claim
|
|
95
|
+
# groups: :realm_roles # X-Auth-Request-Groups ⊆ JWT realm_access.roles
|
|
96
|
+
# }
|
|
97
|
+
|
|
98
|
+
# :enforce (default) - Reject mismatches with 403
|
|
99
|
+
# :log_only - Log mismatches but allow request to proceed
|
|
100
|
+
#
|
|
101
|
+
# config.claims_consistency_mode = :enforce
|
|
102
|
+
|
|
103
|
+
# ==========================================================================
|
|
104
|
+
# Header Name Customization
|
|
105
|
+
# ==========================================================================
|
|
106
|
+
# Customize forwarded token header name (if your proxy uses a different header).
|
|
107
|
+
#
|
|
108
|
+
# config.forwarded_header_name = 'HTTP_X_FORWARDED_ACCESS_TOKEN'
|
|
109
|
+
|
|
110
|
+
# Customize X-Auth-Request-* header names.
|
|
111
|
+
#
|
|
112
|
+
# config.auth_request_headers = {
|
|
113
|
+
# email: 'HTTP_X_AUTH_REQUEST_EMAIL',
|
|
114
|
+
# user: 'HTTP_X_AUTH_REQUEST_USER',
|
|
115
|
+
# groups: 'HTTP_X_AUTH_REQUEST_GROUPS'
|
|
116
|
+
# }
|
|
117
|
+
|
|
118
|
+
# Priority list for seeding Authorization header when empty.
|
|
119
|
+
#
|
|
120
|
+
# config.token_header_priority = ['HTTP_X_FORWARDED_ACCESS_TOKEN']
|
|
121
|
+
|
|
122
|
+
# ==========================================================================
|
|
123
|
+
# Logging
|
|
124
|
+
# ==========================================================================
|
|
125
|
+
# Structured logging hook for observability.
|
|
126
|
+
# Payload includes: rid (request_id), sub, kid, iss, aud
|
|
127
|
+
#
|
|
128
|
+
# config.log_with = ->(payload) { Rails.logger.info(payload.to_json) }
|
|
11
129
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Configuration object for verikloak-bff. Holds middleware settings such as
|
|
4
4
|
# proxy trust rules, header consistency policies, and logging options.
|
|
5
5
|
#
|
|
6
|
+
# @!attribute [rw] disabled
|
|
7
|
+
# @return [Boolean] explicitly disable the middleware (pass-through mode)
|
|
6
8
|
# @!attribute [rw] trusted_proxies
|
|
7
9
|
# @return [Array<String, Regexp, Proc>] allowlist of trusted proxy peers
|
|
8
10
|
# @!attribute [rw] prefer_forwarded
|
|
@@ -28,7 +30,7 @@ module Verikloak
|
|
|
28
30
|
module BFF
|
|
29
31
|
# Configuration for Verikloak::BFF middleware (trusted proxies, header policies, logging, etc.).
|
|
30
32
|
class Configuration
|
|
31
|
-
attr_accessor :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
|
|
33
|
+
attr_accessor :disabled, :trusted_proxies, :prefer_forwarded, :require_forwarded_header,
|
|
32
34
|
:enforce_header_consistency, :enforce_claims_consistency,
|
|
33
35
|
:strip_suspicious_headers, :xff_strategy, :clock_skew_leeway,
|
|
34
36
|
:logger, :peer_preference, :auth_request_headers, :log_with,
|
|
@@ -43,6 +45,7 @@ module Verikloak
|
|
|
43
45
|
#
|
|
44
46
|
# @return [void]
|
|
45
47
|
def initialize
|
|
48
|
+
@disabled = false
|
|
46
49
|
@trusted_proxies = []
|
|
47
50
|
@prefer_forwarded = true
|
|
48
51
|
@require_forwarded_header = false
|
|
@@ -96,10 +96,78 @@ module Verikloak
|
|
|
96
96
|
def sanitize_string(value)
|
|
97
97
|
value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').gsub(LOG_CONTROL_CHARS, '')
|
|
98
98
|
end
|
|
99
|
+
|
|
100
|
+
# Describe the source of the chosen token for logging purposes.
|
|
101
|
+
#
|
|
102
|
+
# @param prefer_forwarded [Boolean]
|
|
103
|
+
# @param auth_token [String, nil]
|
|
104
|
+
# @param fwd_token [String, nil]
|
|
105
|
+
# @return [String] "authorization" or "forwarded"
|
|
106
|
+
def token_source(prefer_forwarded, auth_token, fwd_token)
|
|
107
|
+
return 'forwarded' if prefer_forwarded && fwd_token
|
|
108
|
+
|
|
109
|
+
if auth_token && fwd_token
|
|
110
|
+
'authorization'
|
|
111
|
+
else
|
|
112
|
+
(auth_token ? 'authorization' : 'forwarded')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Extract request id for logging from common headers.
|
|
117
|
+
#
|
|
118
|
+
# @param env [Hash]
|
|
119
|
+
# @return [String, nil]
|
|
120
|
+
def request_id(env)
|
|
121
|
+
env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Emit a structured log line if a logger is present.
|
|
125
|
+
#
|
|
126
|
+
# @param env [Hash]
|
|
127
|
+
# @param config [Configuration]
|
|
128
|
+
# @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
|
|
129
|
+
# @param attrs [Hash]
|
|
130
|
+
# @return [void]
|
|
131
|
+
def log_event(env, config, kind, **attrs)
|
|
132
|
+
lg = config.logger || env['rack.logger']
|
|
133
|
+
payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
|
|
134
|
+
sanitized = sanitize_payload(payload)
|
|
135
|
+
if config.log_with.respond_to?(:call)
|
|
136
|
+
begin
|
|
137
|
+
config.log_with.call(sanitized)
|
|
138
|
+
rescue StandardError
|
|
139
|
+
# ignore log hook failures
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
return unless lg
|
|
143
|
+
|
|
144
|
+
msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
|
|
145
|
+
level = (kind == :ok ? :info : :warn)
|
|
146
|
+
lg.public_send(level, msg)
|
|
147
|
+
rescue StandardError
|
|
148
|
+
# no-op on logging errors
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Build a minimal RFC6750-style error response.
|
|
152
|
+
#
|
|
153
|
+
# @param env [Hash]
|
|
154
|
+
# @param config [Configuration]
|
|
155
|
+
# @param error [Verikloak::BFF::Error]
|
|
156
|
+
# @return [Array(Integer, Hash, Array<String>)]
|
|
157
|
+
def respond_with_error(env, config, error)
|
|
158
|
+
log_event(env, config, :error, code: error.code)
|
|
159
|
+
body = { error: error.code, message: error.message }.to_json
|
|
160
|
+
headers = { 'Content-Type' => 'application/json',
|
|
161
|
+
'WWW-Authenticate' => %(Bearer error="#{error.code}", error_description="#{error.message}") }
|
|
162
|
+
[error.http_status, headers, [body]]
|
|
163
|
+
end
|
|
99
164
|
end
|
|
100
165
|
|
|
101
166
|
# Rack middleware that enforces BFF boundary and header/claims consistency.
|
|
102
167
|
class HeaderGuard
|
|
168
|
+
# Error raised when trusted_proxies is not configured and disabled is not explicitly set.
|
|
169
|
+
class ConfigurationError < StandardError; end
|
|
170
|
+
|
|
103
171
|
RequestTokens = Struct.new(:auth, :forwarded, :chosen)
|
|
104
172
|
|
|
105
173
|
# Accept both Rack 2 and Rack 3 builder call styles:
|
|
@@ -109,6 +177,7 @@ module Verikloak
|
|
|
109
177
|
# @param app [#call]
|
|
110
178
|
# @param opts [Hash, nil]
|
|
111
179
|
# @param opts_kw [Hash]
|
|
180
|
+
# @raise [ConfigurationError] when trusted_proxies is not configured and disabled is false
|
|
112
181
|
def initialize(app, opts = nil, **opts_kw)
|
|
113
182
|
@app = app
|
|
114
183
|
# Use a per-instance copy of global config to avoid cross-request/test side effects
|
|
@@ -118,9 +187,15 @@ module Verikloak
|
|
|
118
187
|
combined.merge!(opts_kw) if opts_kw && !opts_kw.empty?
|
|
119
188
|
apply_overrides!(combined)
|
|
120
189
|
|
|
121
|
-
|
|
190
|
+
# Check configuration validity
|
|
191
|
+
validate_configuration!
|
|
192
|
+
end
|
|
122
193
|
|
|
123
|
-
|
|
194
|
+
# Check if the middleware is explicitly disabled.
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
def disabled?
|
|
198
|
+
@config.disabled
|
|
124
199
|
end
|
|
125
200
|
|
|
126
201
|
# Process a Rack request through the BFF header guard pipeline.
|
|
@@ -131,9 +206,14 @@ module Verikloak
|
|
|
131
206
|
# 3. Policy enforcement (forwarded token requirements, consistency checks)
|
|
132
207
|
# 4. Request finalization (Authorization header normalization)
|
|
133
208
|
#
|
|
209
|
+
# When `disabled: true` is set, the middleware passes through without any processing.
|
|
210
|
+
#
|
|
134
211
|
# @param env [Hash]
|
|
135
212
|
# @return [Array(Integer, Hash, Array<#to_s>)] Rack response triple
|
|
136
213
|
def call(env)
|
|
214
|
+
# Pass through if middleware is explicitly disabled
|
|
215
|
+
return @app.call(env) if disabled?
|
|
216
|
+
|
|
137
217
|
# Stage 1: Validate request comes from trusted proxy
|
|
138
218
|
ensure_trusted_proxy!(env)
|
|
139
219
|
|
|
@@ -148,11 +228,28 @@ module Verikloak
|
|
|
148
228
|
|
|
149
229
|
@app.call(env)
|
|
150
230
|
rescue Verikloak::BFF::Error => e
|
|
151
|
-
respond_with_error(env, e)
|
|
231
|
+
HeaderGuardSanitizer.respond_with_error(env, @config, e)
|
|
152
232
|
end
|
|
153
233
|
|
|
154
234
|
private
|
|
155
235
|
|
|
236
|
+
# Validate that required configuration is present.
|
|
237
|
+
# Raises ConfigurationError if trusted_proxies is not configured and disabled is false.
|
|
238
|
+
#
|
|
239
|
+
# @raise [ConfigurationError]
|
|
240
|
+
# @return [void]
|
|
241
|
+
def validate_configuration!
|
|
242
|
+
return if @config.disabled
|
|
243
|
+
|
|
244
|
+
proxies = @config.trusted_proxies
|
|
245
|
+
return unless proxies.nil? || proxies.empty?
|
|
246
|
+
|
|
247
|
+
raise ConfigurationError,
|
|
248
|
+
'trusted_proxies must be configured for Verikloak::BFF::HeaderGuard. ' \
|
|
249
|
+
'Set trusted_proxies to an array of allowed proxy addresses/CIDRs, ' \
|
|
250
|
+
'or set disabled: true to explicitly skip BFF protection.'
|
|
251
|
+
end
|
|
252
|
+
|
|
156
253
|
# Build token state by extracting, validating, and selecting the active token.
|
|
157
254
|
#
|
|
158
255
|
# @param env [Hash]
|
|
@@ -210,63 +307,6 @@ module Verikloak
|
|
|
210
307
|
auth_token || fwd_token
|
|
211
308
|
end
|
|
212
309
|
|
|
213
|
-
# Describe the source of the chosen token for logging purposes.
|
|
214
|
-
#
|
|
215
|
-
# @param auth_token [String, nil]
|
|
216
|
-
# @param fwd_token [String, nil]
|
|
217
|
-
# @return [String] "authorization" or "forwarded"
|
|
218
|
-
def token_source(auth_token, fwd_token)
|
|
219
|
-
return 'forwarded' if @config.prefer_forwarded && fwd_token
|
|
220
|
-
|
|
221
|
-
if auth_token && fwd_token
|
|
222
|
-
'authorization'
|
|
223
|
-
else
|
|
224
|
-
(auth_token ? 'authorization' : 'forwarded')
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Extract request id for logging from common headers.
|
|
229
|
-
#
|
|
230
|
-
# @param env [Hash]
|
|
231
|
-
# @return [String, nil]
|
|
232
|
-
def request_id(env)
|
|
233
|
-
env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Resolve logger to use (config-provided or rack.logger).
|
|
237
|
-
#
|
|
238
|
-
# @param env [Hash]
|
|
239
|
-
# @return [Logger, nil]
|
|
240
|
-
def logger(env)
|
|
241
|
-
@config.logger || env['rack.logger']
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Emit a structured log line if a logger is present.
|
|
245
|
-
#
|
|
246
|
-
# @param env [Hash]
|
|
247
|
-
# @param kind [Symbol] :ok, :mismatch, :claims_mismatch, :error
|
|
248
|
-
# @param attrs [Hash]
|
|
249
|
-
# @return [void]
|
|
250
|
-
def log_event(env, kind, **attrs)
|
|
251
|
-
lg = logger(env)
|
|
252
|
-
payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
|
|
253
|
-
sanitized = HeaderGuardSanitizer.sanitize_payload(payload)
|
|
254
|
-
if @config.log_with.respond_to?(:call)
|
|
255
|
-
begin
|
|
256
|
-
@config.log_with.call(sanitized)
|
|
257
|
-
rescue StandardError
|
|
258
|
-
# ignore log hook failures
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
return unless lg
|
|
262
|
-
|
|
263
|
-
msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
|
|
264
|
-
level = (kind == :ok ? :info : :warn)
|
|
265
|
-
lg.public_send(level, msg)
|
|
266
|
-
rescue StandardError
|
|
267
|
-
# no-op on logging errors
|
|
268
|
-
end
|
|
269
|
-
|
|
270
310
|
# Raise when the request did not come through a trusted proxy.
|
|
271
311
|
#
|
|
272
312
|
# @param env [Hash]
|
|
@@ -300,7 +340,7 @@ module Verikloak
|
|
|
300
340
|
digest_b = ::Digest::SHA256.hexdigest(fwd_token)
|
|
301
341
|
return if Rack::Utils.secure_compare(digest_a, digest_b)
|
|
302
342
|
|
|
303
|
-
log_event(env, :mismatch, reason: 'authorization_vs_forwarded')
|
|
343
|
+
HeaderGuardSanitizer.log_event(env, @config, :mismatch, reason: 'authorization_vs_forwarded')
|
|
304
344
|
raise HeaderMismatchError
|
|
305
345
|
end
|
|
306
346
|
|
|
@@ -315,7 +355,7 @@ module Verikloak
|
|
|
315
355
|
return unless res.is_a?(Array) && res.first == :error
|
|
316
356
|
|
|
317
357
|
field = res.last
|
|
318
|
-
log_event(env, :claims_mismatch, field: field.to_s)
|
|
358
|
+
HeaderGuardSanitizer.log_event(env, @config, :claims_mismatch, field: field.to_s)
|
|
319
359
|
return if claims_consistency_log_only?
|
|
320
360
|
|
|
321
361
|
raise ClaimsMismatchError, field
|
|
@@ -342,20 +382,8 @@ module Verikloak
|
|
|
342
382
|
return unless chosen
|
|
343
383
|
|
|
344
384
|
ForwardedToken.set_authorization!(env, chosen)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# Build a minimal RFC6750-style error response.
|
|
349
|
-
#
|
|
350
|
-
# @param env [Hash]
|
|
351
|
-
# @param error [Verikloak::BFF::Error]
|
|
352
|
-
# @return [Array(Integer, Hash, Array<String>)]
|
|
353
|
-
def respond_with_error(env, error)
|
|
354
|
-
log_event(env, :error, code: error.code)
|
|
355
|
-
body = { error: error.code, message: error.message }.to_json
|
|
356
|
-
headers = { 'Content-Type' => 'application/json',
|
|
357
|
-
'WWW-Authenticate' => %(Bearer error="#{error.code}", error_description="#{error.message}") }
|
|
358
|
-
[error.http_status, headers, [body]]
|
|
385
|
+
source = HeaderGuardSanitizer.token_source(@config.prefer_forwarded, auth_token, fwd_token)
|
|
386
|
+
HeaderGuardSanitizer.log_event(env, @config, :ok, source: source, **HeaderGuardSanitizer.token_tags(chosen))
|
|
359
387
|
end
|
|
360
388
|
|
|
361
389
|
# Resolve the first env header from which to source a bearer token.
|
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -55,7 +55,7 @@ dependencies:
|
|
|
55
55
|
requirements:
|
|
56
56
|
- - ">="
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: 0.
|
|
58
|
+
version: 0.3.0
|
|
59
59
|
- - "<"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: 1.0.0
|
|
@@ -65,7 +65,7 @@ dependencies:
|
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
68
|
+
version: 0.3.0
|
|
69
69
|
- - "<"
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
71
|
version: 1.0.0
|
|
@@ -101,7 +101,7 @@ metadata:
|
|
|
101
101
|
source_code_uri: https://github.com/taiyaky/verikloak-bff
|
|
102
102
|
changelog_uri: https://github.com/taiyaky/verikloak-bff/blob/main/CHANGELOG.md
|
|
103
103
|
bug_tracker_uri: https://github.com/taiyaky/verikloak-bff/issues
|
|
104
|
-
documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.
|
|
104
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-bff/0.3.0
|
|
105
105
|
rubygems_mfa_required: 'true'
|
|
106
106
|
rdoc_options: []
|
|
107
107
|
require_paths:
|