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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d7d44a12b8963c944bc63c16ed0890ce63df67f5f6226f1db61aac701dbbc00
4
- data.tar.gz: ff24126d34ee02f4eaa220c97db88e7f379657c9a26ece23f868be6ad46ecb9b
3
+ metadata.gz: 1add58266f9789a0e9263c36d5e33ab80207b02a87b999d22b5fa65b813f5f36
4
+ data.tar.gz: 58d956084cc1631e4a0854744627dd3ba95350f7a9c1687665b7025fd943addb
5
5
  SHA512:
6
- metadata.gz: 968b2516670f1d4dd17dc3a4c2ddbb398b26ed0bc93007612ec4ee35c84d2066b3458e3d790f4e6211183d76daffa605464d9cf645da3050ad2e34af0468f4a0
7
- data.tar.gz: dc05a2a815b3be7d9d67921bcc2bc2ddcbd9afdd71264de0f421e1b666906e862cc0963f64fef883a89ac66025c27abd10de967f77ff8d32665d81e895b28c57
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` | Renders standardized 403 JSON when requirements are not met |
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 Middlewares
85
+ ### Inserted Middleware
86
86
  | Component | Inserted after | Purpose |
87
87
  | --- | --- | --- |
88
- | `Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken` | `Rails::Rack::Logger` | Promote trusted `X-Forwarded-Access-Token` to `Authorization` and, when `Authorization` is empty, set it from a prioritized list of headers |
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
- ### Header Sources and Trust
92
- - Never overwrites an existing `Authorization` header.
93
- - Considers `X-Forwarded-Access-Token` only when both are true: `config.verikloak.trust_forwarded_access_token` is enabled and the direct peer IP is within `config.verikloak.trusted_proxy_subnets`.
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
- ### BFF Header Promotion
98
- When fronted by a BFF (e.g., oauth2-proxy) that injects `X-Forwarded-Access-Token`, you can promote that header to `Authorization` from trusted sources only.
94
+ - Gem: https://github.com/taiyaky/verikloak-bff
95
+ - Rails guide: `docs/rails.md` in that repository
99
96
 
100
- Enable promotion and restrict to trusted subnets:
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` and render JSON 500 | `false` |
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
- # Leave header promotion off unless you run a trusted BFF/proxy
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`, `trusted_proxy_subnets`, `token_header_priority`), prefer defining Ruby arrays in the initializer. If passing via ENV, use comma-separated strings and parse in the initializer.
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] token_header_priority
47
- # Env header keys in priority order for sourcing bearer token.
48
- # @return [Array<String>]
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, :trust_forwarded_access_token,
51
- :logger_tags, :error_renderer, :auto_include_controller, :render_500_json, :token_header_priority,
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 |_e|
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
- render json: { error: 'forbidden', message: 'Required audience not satisfied' }, status: :forbidden
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 middleware (`ForwardedAccessToken`, then `Verikloak::Middleware`)
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 trust_forwarded_access_token
22
- trusted_proxy_subnets logger_tags error_renderer auto_include_controller
23
- render_500_json token_header_priority rescue_pundit].each do |key|
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Verikloak
4
4
  module Rails
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end
@@ -4,7 +4,6 @@ require 'verikloak/rails/version'
4
4
  require 'verikloak/rails/configuration'
5
5
  require 'verikloak/rails/error_renderer'
6
6
  require 'verikloak/rails/controller'
7
- require 'verikloak/rails/middleware_integration'
8
7
  require 'verikloak/rails/railtie'
9
8
 
10
9
  module Verikloak
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.1.1
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, JSON errors,
53
- BFF header support.'
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.1.1
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