verikloak-rails 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +50 -17
- data/README.md +16 -64
- data/lib/verikloak/rails/configuration.rb +8 -24
- data/lib/verikloak/rails/controller.rb +61 -8
- data/lib/verikloak/rails/error_renderer.rb +5 -2
- data/lib/verikloak/rails/railtie.rb +4 -9
- data/lib/verikloak/rails/version.rb +1 -1
- data/lib/verikloak/rails.rb +0 -1
- metadata +4 -5
- data/lib/verikloak/rails/middleware_integration.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1add58266f9789a0e9263c36d5e33ab80207b02a87b999d22b5fa65b813f5f36
|
4
|
+
data.tar.gz: 58d956084cc1631e4a0854744627dd3ba95350f7a9c1687665b7025fd943addb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 183bb26475f75704c35880db14e67ea5a83a3c3923d3ca8c6788c92e7735a2d00deabb681f0e65b8bf561e2b6a5aa0cfb4e7ccc24760c091dbe0145f87558311
|
7
|
+
data.tar.gz: 24bc9a344903243e2c63015c4429b8ea713467df15636021f91fc4698a39600ecd0a84e9caccac489ffc1e1d809353961170008ceabed24a106ad65e1b720fef
|
data/CHANGELOG.md
CHANGED
@@ -7,34 +7,67 @@ 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-21
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
- Simplify `with_required_audience!` to always raise `Verikloak::Error`, letting the shared handler render forbidden responses
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
- Ensure the 500 JSON renderer logs exceptions against the actual Rails logger even when wrapped by tagged logging adapters
|
17
|
+
|
18
|
+
### Documentation
|
19
|
+
- Describe the `rescue_pundit` configuration flag and default initializer settings
|
20
|
+
|
21
|
+
## [0.2.0] - 2025-09-14
|
22
|
+
|
23
|
+
### Breaking
|
24
|
+
- Extracted BFF-related functionality into a separate gem, "verikloak-bff". This gem no longer ships BFF-specific middleware or configuration
|
25
|
+
|
26
|
+
### Removed
|
27
|
+
- Middleware `Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken`
|
28
|
+
- Configuration keys: `config.verikloak.trust_forwarded_access_token`, `config.verikloak.trusted_proxy_subnets`, `config.verikloak.token_header_priority`
|
29
|
+
- Corresponding entries in the installer initializer template
|
30
|
+
- BFF-related sections in README and related unit/integration specs
|
31
|
+
|
32
|
+
### Migration
|
33
|
+
1. Add `gem 'verikloak-bff'` to your application Gemfile and bundle
|
34
|
+
2. Create an initializer (e.g., `config/initializers/verikloak_bff.rb`) and configure `Verikloak::BFF` (e.g., `trusted_proxies`, header/claims consistency)
|
35
|
+
3. Insert the BFF middleware before the core `Verikloak::Middleware`:
|
36
|
+
```ruby
|
37
|
+
config.middleware.insert_before Verikloak::Middleware, Verikloak::BFF::HeaderGuard
|
38
|
+
```
|
39
|
+
4. Remove any old `config.verikloak.*` BFF options from your app config; they are no longer used by this gem
|
40
|
+
|
41
|
+
Reference: verikloak-bff https://github.com/taiyaky/verikloak-bff (Rails guide: `docs/rails.md`)
|
42
|
+
|
10
43
|
## [0.1.1] - 2025-09-13
|
11
44
|
|
12
45
|
### Fixed
|
13
|
-
- ForwardedAccessToken: fix `ensure_bearer` accepting malformed values (e.g., `BearerXYZ`)
|
46
|
+
- ForwardedAccessToken: fix `ensure_bearer` accepting malformed values (e.g., `BearerXYZ`)
|
14
47
|
|
15
48
|
### Changed
|
16
|
-
- Strengthen Bearer scheme normalization to always produce `Bearer <token
|
17
|
-
- Detect scheme case-insensitively
|
18
|
-
- Collapse tabs/multiple spaces after the scheme to a single space
|
19
|
-
- Normalize missing-space form `BearerXYZ` to `Bearer XYZ
|
20
|
-
- Add/update middleware specs to cover the above normalization
|
49
|
+
- Strengthen Bearer scheme normalization to always produce `Bearer <token>`
|
50
|
+
- Detect scheme case-insensitively
|
51
|
+
- Collapse tabs/multiple spaces after the scheme to a single space
|
52
|
+
- Normalize missing-space form `BearerXYZ` to `Bearer XYZ`
|
53
|
+
- Add/update middleware specs to cover the above normalization
|
21
54
|
|
22
55
|
## [0.1.0] - 2025-09-07
|
23
56
|
|
24
57
|
### Added
|
25
|
-
- Initial release of `verikloak-rails` (Rails integration for Verikloak)
|
26
|
-
- Railtie auto-wiring via `config.verikloak.*` and installer generator `rails g verikloak:install
|
58
|
+
- Initial release of `verikloak-rails` (Rails integration for Verikloak)
|
59
|
+
- Railtie auto-wiring via `config.verikloak.*` and installer generator `rails g verikloak:install`
|
27
60
|
- Controller concern with authentication helpers:
|
28
61
|
- `before_action :authenticate_user!`
|
29
62
|
- `current_user_claims`, `current_subject`, `current_token`, `authenticated?`, `with_required_audience!`
|
30
63
|
- Rack middleware integration:
|
31
|
-
- Auto-inserts `Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken` and base `Verikloak::Middleware
|
32
|
-
- Optional BFF header promotion from `X-Forwarded-Access-Token` to `Authorization`, gated by `trust_forwarded_access_token` and `trusted_proxy_subnets` (never overwrites existing `Authorization`)
|
33
|
-
- Token sourcing priority configurable via `token_header_priority
|
64
|
+
- Auto-inserts `Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken` and base `Verikloak::Middleware`
|
65
|
+
- Optional BFF header promotion from `X-Forwarded-Access-Token` to `Authorization`, gated by `trust_forwarded_access_token` and `trusted_proxy_subnets` (never overwrites existing `Authorization`)
|
66
|
+
- Token sourcing priority configurable via `token_header_priority`
|
34
67
|
- Consistent JSON error responses and statuses:
|
35
|
-
- 401/403/503 standardized; `WWW-Authenticate` header on 401
|
36
|
-
- Optional global 500 JSON via `config.verikloak.render_500_json
|
37
|
-
- Optional Pundit integration: rescue `Pundit::NotAuthorizedError` to 403 JSON (`config.verikloak.rescue_pundit`)
|
38
|
-
- Request logging tags (`:request_id`, `:sub`) via `config.verikloak.logger_tags
|
39
|
-
- Configurable initializer: `discovery_url`, `audience`, `issuer`, `leeway`, `skip_paths`, `trust_forwarded_access_token`, `trusted_proxy_subnets`, `auto_include_controller`, `error_renderer`, and more
|
40
|
-
- Compatibility: Ruby >= 3.1, Rails 6.1–8.x; depends on `verikloak` >= 0.1.2, < 0.2
|
68
|
+
- 401/403/503 standardized; `WWW-Authenticate` header on 401
|
69
|
+
- Optional global 500 JSON via `config.verikloak.render_500_json`
|
70
|
+
- Optional Pundit integration: rescue `Pundit::NotAuthorizedError` to 403 JSON (`config.verikloak.rescue_pundit`)
|
71
|
+
- Request logging tags (`:request_id`, `:sub`) via `config.verikloak.logger_tags`
|
72
|
+
- Configurable initializer: `discovery_url`, `audience`, `issuer`, `leeway`, `skip_paths`, `trust_forwarded_access_token`, `trusted_proxy_subnets`, `auto_include_controller`, `error_renderer`, and more
|
73
|
+
- Compatibility: Ruby >= 3.1, Rails 6.1–8.x; depends on `verikloak` >= 0.1.2, < 0.2
|
data/README.md
CHANGED
@@ -40,7 +40,7 @@ Then configure `config/initializers/verikloak.rb`.
|
|
40
40
|
| `current_user_claims` | Verified JWT claims (string keys) | `Hash` or `nil` | — |
|
41
41
|
| `current_subject` | Convenience accessor for `sub` claim | `String` or `nil` | — |
|
42
42
|
| `current_token` | Raw Bearer token from the request | `String` or `nil` | — |
|
43
|
-
| `with_required_audience!(*aud)` | Enforce that `aud` includes all required entries | `void` |
|
43
|
+
| `with_required_audience!(*aud)` | Enforce that `aud` includes all required entries | `void` | Raises `Verikloak::Error('forbidden')` so the concern renders standardized 403 JSON and halts the action |
|
44
44
|
|
45
45
|
### Data Sources
|
46
46
|
| Value | Rack env keys | Fallback (RequestStore) | Notes |
|
@@ -82,48 +82,19 @@ end
|
|
82
82
|
```
|
83
83
|
|
84
84
|
## Middleware
|
85
|
-
### Inserted
|
85
|
+
### Inserted Middleware
|
86
86
|
| Component | Inserted after | Purpose |
|
87
87
|
| --- | --- | --- |
|
88
|
-
| `Verikloak::
|
89
|
-
| `Verikloak::Middleware` | `ForwardedAccessToken` | Validate Bearer JWT (OIDC discovery + JWKS), set `verikloak.user`/`verikloak.token`, and honor `skip_paths` |
|
88
|
+
| `Verikloak::Middleware` | `Rails::Rack::Logger` | Validate Bearer JWT (OIDC discovery + JWKS), set `verikloak.user`/`verikloak.token`, and honor `skip_paths` |
|
90
89
|
|
91
|
-
###
|
92
|
-
|
93
|
-
-
|
94
|
-
- `config.verikloak.token_header_priority` decides which env header can seed `Authorization` when it is empty. Note: `HTTP_AUTHORIZATION` is ignored as a source (it is the target header); include other headers if you need additional sources. Forwarded headers are skipped if not trusted.
|
95
|
-
- Direct peer detection prefers `REMOTE_ADDR`, falling back to the nearest proxy in `X-Forwarded-For` when needed.
|
90
|
+
### BFF Integration
|
91
|
+
Support for BFF header handling (e.g., normalizing or enforcing `X-Forwarded-Access-Token`) now lives in a dedicated gem: verikloak-bff.
|
92
|
+
Note: verikloak-bff's `HeaderGuard` never overwrites an existing `Authorization` header.
|
96
93
|
|
97
|
-
|
98
|
-
|
94
|
+
- Gem: https://github.com/taiyaky/verikloak-bff
|
95
|
+
- Rails guide: `docs/rails.md` in that repository
|
99
96
|
|
100
|
-
|
101
|
-
|
102
|
-
```ruby
|
103
|
-
Rails.application.configure do
|
104
|
-
config.verikloak.trust_forwarded_access_token = true
|
105
|
-
config.verikloak.trusted_proxy_subnets = [
|
106
|
-
'10.0.0.0/8',
|
107
|
-
'192.168.0.0/16'
|
108
|
-
]
|
109
|
-
end
|
110
|
-
```
|
111
|
-
|
112
|
-
### Reordering or Disabling (advanced)
|
113
|
-
You can adjust the stack in an initializer after the gem loads, for example:
|
114
|
-
|
115
|
-
```ruby
|
116
|
-
Rails.application.configure do
|
117
|
-
# Remove header-promotion middleware if you never use BFF tokens
|
118
|
-
config.middleware.delete Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken
|
119
|
-
|
120
|
-
# Or move the middleware earlier/later if your stack requires it
|
121
|
-
# config.middleware.insert_before SomeOtherMiddleware, Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken,
|
122
|
-
# trust_forwarded: Verikloak::Rails.config.trust_forwarded_access_token,
|
123
|
-
# trusted_proxies: Verikloak::Rails.config.trusted_proxy_subnets,
|
124
|
-
# header_priority: Verikloak::Rails.config.token_header_priority
|
125
|
-
end
|
126
|
-
```
|
97
|
+
Use verikloak-bff alongside this gem when you front Rails with a BFF/proxy such as oauth2-proxy and need to enforce trusted forwarding and header consistency.
|
127
98
|
|
128
99
|
## Configuration (initializer)
|
129
100
|
### Keys
|
@@ -136,13 +107,10 @@ Keys under `config.verikloak`:
|
|
136
107
|
| `issuer` | String | Expected `iss` | `nil` |
|
137
108
|
| `leeway` | Integer | Clock skew allowance (seconds) | `60` |
|
138
109
|
| `skip_paths` | Array<String> | Paths to skip verification | `['/up','/health','/rails/health']` |
|
139
|
-
| `trust_forwarded_access_token` | Boolean | Trust `X-Forwarded-Access-Token` from trusted proxies | `false` |
|
140
|
-
| `trusted_proxy_subnets` | Array<String or IPAddr> | Subnets allowed to be treated as trusted | `[]` (treat all as trusted; set explicit ranges in production) |
|
141
110
|
| `logger_tags` | Array<Symbol> | Tags to add to Rails logs. Supports `:request_id`, `:sub` | `[:request_id, :sub]` |
|
142
111
|
| `error_renderer` | Object responding to `render(controller, error)` | Override error rendering | built-in JSON renderer |
|
143
112
|
| `auto_include_controller` | Boolean | Auto-include controller concern | `true` |
|
144
|
-
| `render_500_json` | Boolean | Rescue `StandardError
|
145
|
-
| `token_header_priority` | Array<String> | Env header priority to source bearer token | `['HTTP_X_FORWARDED_ACCESS_TOKEN','HTTP_AUTHORIZATION']` |
|
113
|
+
| `render_500_json` | Boolean | Rescue `StandardError`, log the exception, and render JSON 500 | `false` |
|
146
114
|
| `rescue_pundit` | Boolean | Rescue `Pundit::NotAuthorizedError` to 403 JSON when Pundit is present | `true` |
|
147
115
|
|
148
116
|
Environment variable examples are in the generated initializer.
|
@@ -150,7 +118,6 @@ Environment variable examples are in the generated initializer.
|
|
150
118
|
### Minimum Setup
|
151
119
|
- Required: set `discovery_url` to your provider’s OIDC discovery document URL.
|
152
120
|
- Recommended: set `audience` (expected `aud`), and `issuer` when known.
|
153
|
-
- Optional: enable BFF header promotion only with explicit `trusted_proxy_subnets`.
|
154
121
|
|
155
122
|
```ruby
|
156
123
|
# config/initializers/verikloak.rb
|
@@ -160,15 +127,12 @@ Rails.application.configure do
|
|
160
127
|
# Optional but recommended when you know it
|
161
128
|
# config.verikloak.issuer = 'https://idp.example.com/realms/myrealm'
|
162
129
|
|
163
|
-
#
|
164
|
-
# config.verikloak.trust_forwarded_access_token = false
|
165
|
-
# config.verikloak.trusted_proxy_subnets = ['10.0.0.0/8']
|
130
|
+
# For BFF/proxy header handling, see verikloak-bff
|
166
131
|
end
|
167
132
|
```
|
168
133
|
|
169
134
|
Notes:
|
170
|
-
- For array-like values (`audience`, `skip_paths
|
171
|
-
- Header sourcing/trust behavior is described in “Middleware → Header Sources and Trust”.
|
135
|
+
- For array-like values (`audience`, `skip_paths`), prefer defining Ruby arrays in the initializer. If passing via ENV, use comma-separated strings and parse in the initializer.
|
172
136
|
|
173
137
|
### Full Example (selected options)
|
174
138
|
```ruby
|
@@ -179,16 +143,8 @@ Rails.application.configure do
|
|
179
143
|
config.verikloak.leeway = Integer(ENV.fetch('VERIKLOAK_LEEWAY', '60'))
|
180
144
|
config.verikloak.skip_paths = %w[/up /health /rails/health]
|
181
145
|
|
182
|
-
# Enable BFF header promotion only from trusted subnets
|
183
|
-
config.verikloak.trust_forwarded_access_token = ENV.fetch('VERIKLOAK_TRUST_FWD_TOKEN', 'false') == 'true'
|
184
|
-
config.verikloak.trusted_proxy_subnets = [
|
185
|
-
'10.0.0.0/8', # internal LB
|
186
|
-
# '192.168.0.0/16'
|
187
|
-
]
|
188
|
-
|
189
146
|
config.verikloak.logger_tags = %i[request_id sub]
|
190
147
|
config.verikloak.render_500_json = ENV.fetch('VERIKLOAK_RENDER_500', 'false') == 'true'
|
191
|
-
config.verikloak.token_header_priority = %w[HTTP_X_FORWARDED_ACCESS_TOKEN HTTP_AUTHORIZATION]
|
192
148
|
|
193
149
|
# Optional Pundit rescue (403 JSON)
|
194
150
|
config.verikloak.rescue_pundit = ENV.fetch('VERIKLOAK_RESCUE_PUNDIT', 'true') == 'true'
|
@@ -203,14 +159,9 @@ end
|
|
203
159
|
| `audience` | `VERIKLOAK_AUDIENCE` |
|
204
160
|
| `issuer` | `VERIKLOAK_ISSUER` |
|
205
161
|
| `leeway` | `VERIKLOAK_LEEWAY` |
|
206
|
-
| `trust_forwarded_access_token` | `VERIKLOAK_TRUST_FWD_TOKEN` |
|
207
162
|
| `render_500_json` | `VERIKLOAK_RENDER_500` |
|
208
163
|
| `rescue_pundit` | `VERIKLOAK_RESCUE_PUNDIT` |
|
209
164
|
|
210
|
-
### Notes
|
211
|
-
- Default for `trust_forwarded_access_token` is secure (`false`). Set `trusted_proxy_subnets` before enabling.
|
212
|
-
- The middleware never overwrites an existing `Authorization` header.
|
213
|
-
- If `trusted_proxy_subnets` is empty, all peers are treated as trusted. In production, set explicit subnets or keep `trust_forwarded_access_token` disabled.
|
214
165
|
|
215
166
|
## Errors
|
216
167
|
This gem standardizes JSON error responses and HTTP statuses. See [ERRORS.md](ERRORS.md) for details and examples.
|
@@ -220,7 +171,7 @@ This gem standardizes JSON error responses and HTTP statuses. See [ERRORS.md](ER
|
|
220
171
|
| --- | --- | --- | --- | --- |
|
221
172
|
| 401 Unauthorized | `invalid_token`, `unauthorized` | Missing/invalid Bearer token; failed signature/expiry/issuer/audience checks | `WWW-Authenticate: Bearer` with optional `error` and `error_description` | `{ "error": "invalid_token", "message": "token expired" }` |
|
222
173
|
| 403 Forbidden | `forbidden` | Audience check failure via `with_required_audience!`; optionally `Pundit::NotAuthorizedError` when rescue is enabled | — | `{ "error": "forbidden", "message": "Required audience not satisfied" }` |
|
223
|
-
| 503 Service Unavailable | `jwks_fetch_failed`, `jwks_parse_failed`, `discovery_metadata_fetch_failed`, `discovery_metadata_invalid` | Upstream metadata/JWKS issues | — | `{ "error": "jwks_fetch_failed", "message": "..." }` |
|
174
|
+
| 503 Service Unavailable | `jwks_fetch_failed`, `jwks_parse_failed`, `discovery_metadata_fetch_failed`, `discovery_metadata_invalid`, `invalid_discovery_url`, `discovery_redirect_error` | Upstream metadata/JWKS issues | — | `{ "error": "jwks_fetch_failed", "message": "..." }` |
|
224
175
|
|
225
176
|
### Customize
|
226
177
|
Customize rendering by assigning `config.verikloak.error_renderer`.
|
@@ -261,8 +212,8 @@ Note: Always sanitize values placed into `WWW-Authenticate` header parameters to
|
|
261
212
|
class CompactErrorRenderer
|
262
213
|
private
|
263
214
|
def sanitize_quoted(val)
|
264
|
-
# Escape quotes/backslashes and strip CR/LF
|
265
|
-
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]
|
215
|
+
# Escape quotes/backslashes and strip CR/LF (collapse runs to a single space)
|
216
|
+
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]+/, ' ')
|
266
217
|
end
|
267
218
|
end
|
268
219
|
```
|
@@ -338,5 +289,6 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
338
289
|
|
339
290
|
## References
|
340
291
|
- verikloak-rails (this gem): https://rubygems.org/gems/verikloak-rails
|
292
|
+
- verikloak-bff: https://rubygems.org/gems/verikloak-bff
|
341
293
|
- Verikloak (base gem): https://github.com/taiyaky/verikloak
|
342
294
|
- Verikloak on RubyGems: https://rubygems.org/gems/verikloak
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'ipaddr'
|
4
|
-
|
5
3
|
module Verikloak
|
6
4
|
module Rails
|
7
5
|
# Configuration for verikloak-rails.
|
@@ -25,12 +23,6 @@ module Verikloak
|
|
25
23
|
# @!attribute [rw] skip_paths
|
26
24
|
# Paths to skip verification.
|
27
25
|
# @return [Array<String>]
|
28
|
-
# @!attribute [rw] trust_forwarded_access_token
|
29
|
-
# Whether to trust `X-Forwarded-Access-Token` from trusted proxies.
|
30
|
-
# @return [Boolean]
|
31
|
-
# @!attribute [r] trusted_proxy_subnets
|
32
|
-
# Trusted proxy subnets.
|
33
|
-
# @return [Array<IPAddr>]
|
34
26
|
# @!attribute [rw] logger_tags
|
35
27
|
# Log tags to include (supports :request_id, :sub).
|
36
28
|
# @return [Array<Symbol>]
|
@@ -43,37 +35,29 @@ module Verikloak
|
|
43
35
|
# @!attribute [rw] render_500_json
|
44
36
|
# Rescue StandardError and render a JSON 500 response.
|
45
37
|
# @return [Boolean]
|
46
|
-
# @!attribute [rw]
|
47
|
-
#
|
48
|
-
# @return [
|
38
|
+
# @!attribute [rw] rescue_pundit
|
39
|
+
# Rescue `Pundit::NotAuthorizedError` and render JSON 403 responses.
|
40
|
+
# @return [Boolean]
|
49
41
|
class Configuration
|
50
|
-
attr_accessor :discovery_url, :audience, :issuer, :leeway, :skip_paths,
|
51
|
-
:logger_tags, :error_renderer, :auto_include_controller,
|
52
|
-
:rescue_pundit
|
53
|
-
attr_reader :trusted_proxy_subnets
|
42
|
+
attr_accessor :discovery_url, :audience, :issuer, :leeway, :skip_paths,
|
43
|
+
:logger_tags, :error_renderer, :auto_include_controller,
|
44
|
+
:render_500_json, :rescue_pundit
|
54
45
|
|
46
|
+
# Initialize configuration with sensible defaults for Rails apps.
|
47
|
+
# @return [void]
|
55
48
|
def initialize
|
56
49
|
@discovery_url = nil
|
57
50
|
@audience = nil
|
58
51
|
@issuer = nil
|
59
52
|
@leeway = 60
|
60
53
|
@skip_paths = ['/up', '/health', '/rails/health']
|
61
|
-
@trust_forwarded_access_token = false
|
62
|
-
@trusted_proxy_subnets = []
|
63
54
|
@logger_tags = %i[request_id sub]
|
64
55
|
@error_renderer = Verikloak::Rails::ErrorRenderer.new
|
65
56
|
@auto_include_controller = true
|
66
57
|
@render_500_json = false
|
67
|
-
@token_header_priority = %w[HTTP_X_FORWARDED_ACCESS_TOKEN HTTP_AUTHORIZATION]
|
68
58
|
@rescue_pundit = true
|
69
59
|
end
|
70
60
|
|
71
|
-
# Assign trusted proxy subnets.
|
72
|
-
# @param list [Array<String, IPAddr>, String, IPAddr] one or more CIDRs or IPAddr instances
|
73
|
-
def trusted_proxy_subnets=(list)
|
74
|
-
@trusted_proxy_subnets = Array(list).map { |e| e.is_a?(IPAddr) ? e : IPAddr.new(e) }
|
75
|
-
end
|
76
|
-
|
77
61
|
# Options forwarded to the base Verikloak Rack middleware.
|
78
62
|
# @return [Hash]
|
79
63
|
# @example
|
@@ -16,7 +16,8 @@ module Verikloak
|
|
16
16
|
before_action :authenticate_user!
|
17
17
|
# Register generic error handler first so specific handlers take precedence.
|
18
18
|
if Verikloak::Rails.config.render_500_json
|
19
|
-
rescue_from StandardError do |
|
19
|
+
rescue_from StandardError do |e|
|
20
|
+
_verikloak_log_internal_error(e)
|
20
21
|
render json: { error: 'internal_server_error', message: 'An unexpected error occurred' },
|
21
22
|
status: :internal_server_error
|
22
23
|
end
|
@@ -84,13 +85,14 @@ module Verikloak
|
|
84
85
|
#
|
85
86
|
# @param required [Array<String>] one or more audiences to require
|
86
87
|
# @return [void]
|
88
|
+
# @raise [Verikloak::Error] when the required audience is missing
|
87
89
|
# @example
|
88
90
|
# with_required_audience!('my-api', 'payments')
|
89
91
|
def with_required_audience!(*required)
|
90
92
|
aud = Array(current_user_claims&.dig('aud'))
|
91
93
|
return if required.flatten.all? { |r| aud.include?(r) }
|
92
94
|
|
93
|
-
|
95
|
+
raise ::Verikloak::Error.new('forbidden', 'Required audience not satisfied')
|
94
96
|
end
|
95
97
|
|
96
98
|
private
|
@@ -99,18 +101,69 @@ module Verikloak
|
|
99
101
|
# @yieldreturn [Object] result of the block
|
100
102
|
# @return [Object]
|
101
103
|
def _verikloak_tag_logs(&)
|
102
|
-
tags =
|
103
|
-
if Verikloak::Rails.config.logger_tags.include?(:request_id)
|
104
|
-
rid = request.request_id || request.headers['X-Request-Id']
|
105
|
-
tags << "req:#{rid}" if rid
|
106
|
-
end
|
107
|
-
tags << "sub:#{current_subject}" if Verikloak::Rails.config.logger_tags.include?(:sub) && current_subject
|
104
|
+
tags = _verikloak_build_log_tags
|
108
105
|
if ::Rails.logger.respond_to?(:tagged) && tags.any?
|
109
106
|
::Rails.logger.tagged(*tags, &)
|
110
107
|
else
|
111
108
|
yield
|
112
109
|
end
|
113
110
|
end
|
111
|
+
|
112
|
+
# Build log tags from request context with minimal branching and safe values.
|
113
|
+
# @return [Array<String>]
|
114
|
+
def _verikloak_build_log_tags
|
115
|
+
tags = []
|
116
|
+
if Verikloak::Rails.config.logger_tags.include?(:request_id)
|
117
|
+
rid = request.request_id || request.headers['X-Request-Id']
|
118
|
+
rid = rid.to_s.gsub(/[\r\n]+/, ' ')
|
119
|
+
tags << "req:#{rid}" unless rid.empty?
|
120
|
+
end
|
121
|
+
if Verikloak::Rails.config.logger_tags.include?(:sub)
|
122
|
+
sub = current_subject
|
123
|
+
if sub
|
124
|
+
sanitized = sub.to_s.gsub(/[[:cntrl:]]+/, ' ').strip
|
125
|
+
tags << "sub:#{sanitized}" unless sanitized.empty?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
tags
|
129
|
+
end
|
130
|
+
|
131
|
+
# Write StandardError details to the controller or Rails logger when
|
132
|
+
# rendering the generic 500 JSON response. Logging ensures the
|
133
|
+
# underlying failure is still visible to operators even though the
|
134
|
+
# response body is static.
|
135
|
+
#
|
136
|
+
# @param exception [Exception]
|
137
|
+
# @return [void]
|
138
|
+
def _verikloak_log_internal_error(exception)
|
139
|
+
target_logger = _verikloak_base_logger
|
140
|
+
return unless target_logger.respond_to?(:error)
|
141
|
+
|
142
|
+
target_logger.error("[Verikloak] #{exception.class}: #{exception.message}")
|
143
|
+
backtrace = exception.backtrace
|
144
|
+
target_logger.error(backtrace.join("\n")) if backtrace&.any?
|
145
|
+
rescue StandardError
|
146
|
+
# Never allow logging failures to interfere with request handling.
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
# Locate the innermost logger that responds to `error`.
|
151
|
+
# @return [Object, nil]
|
152
|
+
def _verikloak_base_logger
|
153
|
+
root_logger = if defined?(::Rails) && ::Rails.respond_to?(:logger)
|
154
|
+
::Rails.logger
|
155
|
+
elsif respond_to?(:logger)
|
156
|
+
logger
|
157
|
+
end
|
158
|
+
current = root_logger
|
159
|
+
while current.respond_to?(:logger)
|
160
|
+
next_logger = current.logger
|
161
|
+
break if next_logger.nil? || next_logger.equal?(current)
|
162
|
+
|
163
|
+
current = next_logger
|
164
|
+
end
|
165
|
+
current
|
166
|
+
end
|
114
167
|
end
|
115
168
|
end
|
116
169
|
end
|
@@ -14,7 +14,10 @@ module Verikloak
|
|
14
14
|
'jwks_fetch_failed' => 503,
|
15
15
|
'jwks_parse_failed' => 503,
|
16
16
|
'discovery_metadata_fetch_failed' => 503,
|
17
|
-
'discovery_metadata_invalid' => 503
|
17
|
+
'discovery_metadata_invalid' => 503,
|
18
|
+
# Additional infrastructure/configuration errors from core
|
19
|
+
'invalid_discovery_url' => 503,
|
20
|
+
'discovery_redirect_error' => 503
|
18
21
|
}.freeze
|
19
22
|
|
20
23
|
# Render an error as JSON, adding `WWW-Authenticate` when appropriate.
|
@@ -77,7 +80,7 @@ module Verikloak
|
|
77
80
|
# @param val [String]
|
78
81
|
# @return [String]
|
79
82
|
def sanitize_quoted(val)
|
80
|
-
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]
|
83
|
+
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]+/, ' ')
|
81
84
|
end
|
82
85
|
end
|
83
86
|
end
|
@@ -8,7 +8,7 @@ module Verikloak
|
|
8
8
|
# Hooks verikloak-rails into a Rails application lifecycle.
|
9
9
|
#
|
10
10
|
# - Applies configuration from `config.verikloak`
|
11
|
-
# - Inserts
|
11
|
+
# - Inserts base `Verikloak::Middleware`
|
12
12
|
# - Auto-includes controller concern when enabled
|
13
13
|
class Railtie < ::Rails::Railtie
|
14
14
|
config.verikloak = ActiveSupport::OrderedOptions.new
|
@@ -18,18 +18,13 @@ module Verikloak
|
|
18
18
|
initializer 'verikloak.configure' do |app|
|
19
19
|
Verikloak::Rails.configure do |c|
|
20
20
|
rails_cfg = app.config.verikloak
|
21
|
-
%i[discovery_url audience issuer leeway skip_paths
|
22
|
-
|
23
|
-
render_500_json
|
21
|
+
%i[discovery_url audience issuer leeway skip_paths
|
22
|
+
logger_tags error_renderer auto_include_controller
|
23
|
+
render_500_json rescue_pundit].each do |key|
|
24
24
|
c.send("#{key}=", rails_cfg[key]) if rails_cfg.key?(key)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
app.middleware.insert_after ::Rails::Rack::Logger,
|
28
|
-
Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken,
|
29
|
-
trust_forwarded: Verikloak::Rails.config.trust_forwarded_access_token,
|
30
|
-
trusted_proxies: Verikloak::Rails.config.trusted_proxy_subnets,
|
31
|
-
header_priority: Verikloak::Rails.config.token_header_priority
|
32
|
-
app.middleware.insert_after Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken,
|
33
28
|
::Verikloak::Middleware,
|
34
29
|
**Verikloak::Rails.config.middleware_options
|
35
30
|
end
|
data/lib/verikloak/rails.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: verikloak-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- taiyaky
|
@@ -49,8 +49,8 @@ dependencies:
|
|
49
49
|
- - "<"
|
50
50
|
- !ruby/object:Gem::Version
|
51
51
|
version: '0.2'
|
52
|
-
description: 'Rails integration for Verikloak: auto middleware, helpers,
|
53
|
-
|
52
|
+
description: 'Rails integration for Verikloak: auto middleware, helpers, and standardized
|
53
|
+
JSON errors.'
|
54
54
|
executables: []
|
55
55
|
extensions: []
|
56
56
|
extra_rdoc_files: []
|
@@ -64,7 +64,6 @@ files:
|
|
64
64
|
- lib/verikloak/rails/configuration.rb
|
65
65
|
- lib/verikloak/rails/controller.rb
|
66
66
|
- lib/verikloak/rails/error_renderer.rb
|
67
|
-
- lib/verikloak/rails/middleware_integration.rb
|
68
67
|
- lib/verikloak/rails/railtie.rb
|
69
68
|
- lib/verikloak/rails/version.rb
|
70
69
|
homepage: https://github.com/taiyaky/verikloak-rails
|
@@ -74,7 +73,7 @@ metadata:
|
|
74
73
|
source_code_uri: https://github.com/taiyaky/verikloak-rails
|
75
74
|
changelog_uri: https://github.com/taiyaky/verikloak-rails/blob/main/CHANGELOG.md
|
76
75
|
bug_tracker_uri: https://github.com/taiyaky/verikloak-rails/issues
|
77
|
-
documentation_uri: https://rubydoc.info/gems/verikloak-rails/0.
|
76
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-rails/0.2.1
|
78
77
|
rubygems_mfa_required: 'true'
|
79
78
|
rdoc_options: []
|
80
79
|
require_paths:
|
@@ -1,126 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ipaddr'
|
4
|
-
|
5
|
-
module Verikloak
|
6
|
-
module Rails
|
7
|
-
# Internal namespace for Rack middleware glue.
|
8
|
-
module MiddlewareIntegration
|
9
|
-
# Promotes forwarded access tokens to Authorization when trusted.
|
10
|
-
#
|
11
|
-
# - Optionally trusts `X-Forwarded-Access-Token` from configured subnets
|
12
|
-
# - Never overwrites an existing `Authorization` header
|
13
|
-
# - Can derive the token from a prioritized list of headers
|
14
|
-
class ForwardedAccessToken
|
15
|
-
BEARER_SCHEME = 'Bearer'
|
16
|
-
BEARER_SCHEME_LENGTH = BEARER_SCHEME.length
|
17
|
-
# Initialize the middleware.
|
18
|
-
#
|
19
|
-
# @param app [#call] next Rack app
|
20
|
-
# @param trust_forwarded [Boolean] whether to trust forwarded access tokens
|
21
|
-
# @param trusted_proxies [Array<IPAddr>, Array<String>] subnets considered trusted
|
22
|
-
# @param header_priority [Array<String>] env header keys to search, in order
|
23
|
-
def initialize(app, trust_forwarded:, trusted_proxies:,
|
24
|
-
header_priority: %w[HTTP_X_FORWARDED_ACCESS_TOKEN HTTP_AUTHORIZATION])
|
25
|
-
@app = app
|
26
|
-
@trust_forwarded = trust_forwarded
|
27
|
-
@trusted_proxies = Array(trusted_proxies)
|
28
|
-
@header_priority = header_priority
|
29
|
-
end
|
30
|
-
|
31
|
-
# Rack entry point: possibly promote a token and pass to the next app.
|
32
|
-
#
|
33
|
-
# @param env [Hash] Rack environment
|
34
|
-
# @return [Array(Integer, Hash, #each)] Rack response triple from downstream app
|
35
|
-
# @example Promote forwarded token
|
36
|
-
# env = { 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_ACCESS_TOKEN' => 'abc' }
|
37
|
-
# status, headers, body = middleware.call(env)
|
38
|
-
def call(env)
|
39
|
-
promote_forwarded_if_trusted(env)
|
40
|
-
first = resolve_first_token_header(env)
|
41
|
-
set_authorization_from(env, first) if first
|
42
|
-
@app.call(env)
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
# Promote X-Forwarded-Access-Token to Authorization when trusted.
|
48
|
-
# @param env [Hash]
|
49
|
-
# @return [void]
|
50
|
-
def promote_forwarded_if_trusted(env)
|
51
|
-
return unless @trust_forwarded && from_trusted_proxy?(env)
|
52
|
-
|
53
|
-
forwarded = env['HTTP_X_FORWARDED_ACCESS_TOKEN']
|
54
|
-
return if forwarded.to_s.empty?
|
55
|
-
return unless env['HTTP_AUTHORIZATION'].to_s.empty?
|
56
|
-
|
57
|
-
env['HTTP_AUTHORIZATION'] = ensure_bearer(forwarded)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Resolve the first header key from which to source a bearer token.
|
61
|
-
# Respects trust policy for forwarded tokens and never returns
|
62
|
-
# 'HTTP_AUTHORIZATION'.
|
63
|
-
#
|
64
|
-
# @param env [Hash]
|
65
|
-
# @return [String, nil]
|
66
|
-
def resolve_first_token_header(env)
|
67
|
-
candidates = @header_priority.dup
|
68
|
-
candidates -= ['HTTP_X_FORWARDED_ACCESS_TOKEN'] unless @trust_forwarded && from_trusted_proxy?(env)
|
69
|
-
candidates.find { |k| (val = env[k]) && !val.to_s.empty? && k != 'HTTP_AUTHORIZATION' }
|
70
|
-
end
|
71
|
-
|
72
|
-
# Set Authorization header from the given env header key.
|
73
|
-
# @param env [Hash]
|
74
|
-
# @param header_key [String]
|
75
|
-
# @return [void]
|
76
|
-
def set_authorization_from(env, header_key)
|
77
|
-
token = env[header_key]
|
78
|
-
env['HTTP_AUTHORIZATION'] ||= ensure_bearer(token)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Normalize to a proper 'Bearer <token>' header value.
|
82
|
-
# - Detects scheme case-insensitively
|
83
|
-
# - Inserts a missing space (e.g., 'BearerXYZ' => 'Bearer XYZ')
|
84
|
-
# - Collapses multiple spaces/tabs after the scheme to a single space
|
85
|
-
# @param token [String]
|
86
|
-
# @return [String]
|
87
|
-
def ensure_bearer(token)
|
88
|
-
s = token.to_s.strip
|
89
|
-
# Case-insensitive 'Bearer' with spaces/tabs after
|
90
|
-
if s =~ /\A#{BEARER_SCHEME}[ \t]+/i
|
91
|
-
rest = s.sub(/\A#{BEARER_SCHEME}[ \t]+/i, '')
|
92
|
-
return "#{BEARER_SCHEME} #{rest}"
|
93
|
-
end
|
94
|
-
|
95
|
-
# Case-insensitive 'Bearer' with no separator (e.g., 'BearerXYZ')
|
96
|
-
if s =~ /\A#{BEARER_SCHEME}(?![ \t])/i
|
97
|
-
rest = s[BEARER_SCHEME_LENGTH..] || ''
|
98
|
-
return "#{BEARER_SCHEME} #{rest}"
|
99
|
-
end
|
100
|
-
|
101
|
-
# No scheme present; add it
|
102
|
-
"#{BEARER_SCHEME} #{s}"
|
103
|
-
end
|
104
|
-
|
105
|
-
# Whether the request originates from a trusted proxy subnet.
|
106
|
-
# @param env [Hash]
|
107
|
-
# @return [Boolean]
|
108
|
-
def from_trusted_proxy?(env)
|
109
|
-
return true if @trusted_proxies.empty?
|
110
|
-
|
111
|
-
# Prefer REMOTE_ADDR (direct peer). Fallback to the nearest proxy from X-Forwarded-For if present.
|
112
|
-
ip = (env['REMOTE_ADDR'] || '').to_s.strip
|
113
|
-
ip = env['HTTP_X_FORWARDED_FOR'].to_s.split(',').last.to_s.strip if ip.empty? && env['HTTP_X_FORWARDED_FOR']
|
114
|
-
return false if ip.empty?
|
115
|
-
|
116
|
-
begin
|
117
|
-
req_ip = IPAddr.new(ip)
|
118
|
-
@trusted_proxies.any? { |subnet| subnet.include?(req_ip) }
|
119
|
-
rescue IPAddr::InvalidAddressError
|
120
|
-
false
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|