verikloak-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +342 -0
- data/lib/generators/verikloak/install/install_generator.rb +37 -0
- data/lib/verikloak/rails/configuration.rb +93 -0
- data/lib/verikloak/rails/controller.rb +116 -0
- data/lib/verikloak/rails/error_renderer.rb +84 -0
- data/lib/verikloak/rails/middleware_integration.rb +107 -0
- data/lib/verikloak/rails/railtie.rb +46 -0
- data/lib/verikloak/rails/version.rb +7 -0
- data/lib/verikloak/rails.rb +41 -0
- data/lib/verikloak-rails.rb +3 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d258ce7b00c8aed380623cc36b26669d86cf80c3c06e748bc677c4f8af93a4f1
|
4
|
+
data.tar.gz: 874e2f3c8ed2eb372d7fd4238639f4fc16e9cdd0ee6a3de7dadc57e24bbcf629
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e035ee3bfa25ad7c9e9eb36d1279f2ea265445c60270aa2d8a8d3fcce1757cad6003892715a1faaec22cb62148bda295522ba6c12cc3bfa7858f3d2100182eb
|
7
|
+
data.tar.gz: c94c073924c9ed1530978aadd7abd84855e7478350572ab70338b9e8cc0a8dd7b739865517dd488b347d87a23739580b09beee206bc8aeeda4afd40d0e5ab13e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
## [0.1.0] - 2025-09-07
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- Initial release of `verikloak-rails` (Rails integration for Verikloak).
|
14
|
+
- Railtie auto-wiring via `config.verikloak.*` and installer generator `rails g verikloak:install`.
|
15
|
+
- Controller concern with authentication helpers:
|
16
|
+
- `before_action :authenticate_user!`
|
17
|
+
- `current_user_claims`, `current_subject`, `current_token`, `authenticated?`, `with_required_audience!`
|
18
|
+
- Rack middleware integration:
|
19
|
+
- Auto-inserts `Verikloak::Rails::MiddlewareIntegration::ForwardedAccessToken` and base `Verikloak::Middleware`.
|
20
|
+
- 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`).
|
21
|
+
- Token sourcing priority configurable via `token_header_priority`.
|
22
|
+
- Consistent JSON error responses and statuses:
|
23
|
+
- 401/403/503 standardized; `WWW-Authenticate` header on 401.
|
24
|
+
- Optional global 500 JSON via `config.verikloak.render_500_json`.
|
25
|
+
- Optional Pundit integration: rescue `Pundit::NotAuthorizedError` to 403 JSON (`config.verikloak.rescue_pundit`).
|
26
|
+
- Request logging tags (`:request_id`, `:sub`) via `config.verikloak.logger_tags`.
|
27
|
+
- Configurable initializer: `discovery_url`, `audience`, `issuer`, `leeway`, `skip_paths`, `trust_forwarded_access_token`, `trusted_proxy_subnets`, `auto_include_controller`, `error_renderer`, and more.
|
28
|
+
- Compatibility: Ruby >= 3.1, Rails 6.1–8.x; depends on `verikloak` >= 0.1.2, < 0.2.
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 taiyaky
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
# verikloak-rails
|
2
|
+
|
3
|
+
[](https://github.com/taiyaky/verikloak-rails/actions/workflows/ci.yml)
|
4
|
+
[](https://rubygems.org/gems/verikloak-rails)
|
5
|
+

|
6
|
+
[](https://rubygems.org/gems/verikloak-rails)
|
7
|
+
|
8
|
+
Rails integration for Verikloak.
|
9
|
+
|
10
|
+
## Purpose
|
11
|
+
Provide drop-in, token-based authentication for Rails APIs via Verikloak (OIDC discovery and JWKS verification). It installs middleware and a controller concern to authenticate Bearer tokens, exposes helpers for claims and subject, and returns standardized JSON error responses (401/403/503) with `WWW-Authenticate` on 401. Defaults prioritize security while keeping configuration minimal.
|
12
|
+
|
13
|
+
## Features
|
14
|
+
- Auto-wiring via Railtie (`config.verikloak.*`)
|
15
|
+
- Controller concern with `before_action :authenticate_user!`
|
16
|
+
- Helpers: `current_user_claims`, `current_subject`, `current_token`, `authenticated?`
|
17
|
+
- Exceptions → standardized JSON (401/403/503) with `WWW-Authenticate` on 401
|
18
|
+
- Log tagging (`request_id`, `sub`)
|
19
|
+
- Installer generator: `rails g verikloak:install`
|
20
|
+
|
21
|
+
## Compatibility
|
22
|
+
- Ruby: >= 3.1
|
23
|
+
- Rails: 6.1 – 8.x
|
24
|
+
- verikloak: >= 0.1.2, < 0.2
|
25
|
+
|
26
|
+
## Quick Start
|
27
|
+
```bash
|
28
|
+
bundle add verikloak verikloak-rails
|
29
|
+
rails g verikloak:install
|
30
|
+
```
|
31
|
+
|
32
|
+
Then configure `config/initializers/verikloak.rb`.
|
33
|
+
|
34
|
+
## Controller Helpers
|
35
|
+
### Available Methods
|
36
|
+
| Method | Purpose | Returns | On failure |
|
37
|
+
| --- | --- | --- | --- |
|
38
|
+
| `authenticate_user!` | Use as a `before_action` to require a valid Bearer token | `void` | Renders standardized 401 JSON and sets `WWW-Authenticate: Bearer` when token is absent/invalid |
|
39
|
+
| `authenticated?` | Whether verified user claims are present | `Boolean` | — |
|
40
|
+
| `current_user_claims` | Verified JWT claims (string keys) | `Hash` or `nil` | — |
|
41
|
+
| `current_subject` | Convenience accessor for `sub` claim | `String` or `nil` | — |
|
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 |
|
44
|
+
|
45
|
+
### Data Sources
|
46
|
+
| Value | Rack env keys | Fallback (RequestStore) | Notes |
|
47
|
+
| --- | --- | --- | --- |
|
48
|
+
| `current_user_claims` | `verikloak.user` | `:verikloak_user` | Uses RequestStore only when available |
|
49
|
+
| `current_token` | `verikloak.token` | `:verikloak_token` | Uses RequestStore only when available |
|
50
|
+
|
51
|
+
### Example Controller
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class ApiController < ApplicationController
|
55
|
+
# Auto-included by default; if disabled, add explicitly:
|
56
|
+
# include Verikloak::Rails::Controller
|
57
|
+
|
58
|
+
def me
|
59
|
+
render json: { sub: current_subject, claims: current_user_claims }
|
60
|
+
end
|
61
|
+
|
62
|
+
def must_have_aud
|
63
|
+
with_required_audience!('my-api')
|
64
|
+
render json: { ok: true }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Manual Include (disable auto-include)
|
70
|
+
If you disable auto-inclusion of the controller concern, add it manually:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# config/initializers/verikloak.rb
|
74
|
+
Rails.application.configure do
|
75
|
+
config.verikloak.auto_include_controller = false
|
76
|
+
end
|
77
|
+
|
78
|
+
# app/controllers/application_controller.rb
|
79
|
+
class ApplicationController < ActionController::Base
|
80
|
+
include Verikloak::Rails::Controller
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
## Middleware
|
85
|
+
### Inserted Middlewares
|
86
|
+
| Component | Inserted after | Purpose |
|
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` |
|
90
|
+
|
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.
|
96
|
+
|
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.
|
99
|
+
|
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
|
+
```
|
127
|
+
|
128
|
+
## Configuration (initializer)
|
129
|
+
### Keys
|
130
|
+
Keys under `config.verikloak`:
|
131
|
+
|
132
|
+
| Key | Type | Description | Default |
|
133
|
+
| --- | --- | --- | --- |
|
134
|
+
| `discovery_url` | String | OIDC discovery URL | `nil` |
|
135
|
+
| `audience` | String or Array | Expected `aud` | `nil` |
|
136
|
+
| `issuer` | String | Expected `iss` | `nil` |
|
137
|
+
| `leeway` | Integer | Clock skew allowance (seconds) | `60` |
|
138
|
+
| `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
|
+
| `logger_tags` | Array<Symbol> | Tags to add to Rails logs. Supports `:request_id`, `:sub` | `[:request_id, :sub]` |
|
142
|
+
| `error_renderer` | Object responding to `render(controller, error)` | Override error rendering | built-in JSON renderer |
|
143
|
+
| `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']` |
|
146
|
+
| `rescue_pundit` | Boolean | Rescue `Pundit::NotAuthorizedError` to 403 JSON when Pundit is present | `true` |
|
147
|
+
|
148
|
+
Environment variable examples are in the generated initializer.
|
149
|
+
|
150
|
+
### Minimum Setup
|
151
|
+
- Required: set `discovery_url` to your provider’s OIDC discovery document URL.
|
152
|
+
- Recommended: set `audience` (expected `aud`), and `issuer` when known.
|
153
|
+
- Optional: enable BFF header promotion only with explicit `trusted_proxy_subnets`.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# config/initializers/verikloak.rb
|
157
|
+
Rails.application.configure do
|
158
|
+
config.verikloak.discovery_url = ENV['KEYCLOAK_DISCOVERY_URL']
|
159
|
+
config.verikloak.audience = ENV.fetch('VERIKLOAK_AUDIENCE', 'rails-api')
|
160
|
+
# Optional but recommended when you know it
|
161
|
+
# config.verikloak.issuer = 'https://idp.example.com/realms/myrealm'
|
162
|
+
|
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']
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
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”.
|
172
|
+
|
173
|
+
### Full Example (selected options)
|
174
|
+
```ruby
|
175
|
+
# config/initializers/verikloak.rb
|
176
|
+
Rails.application.configure do
|
177
|
+
config.verikloak.discovery_url = ENV['KEYCLOAK_DISCOVERY_URL']
|
178
|
+
config.verikloak.audience = ENV.fetch('VERIKLOAK_AUDIENCE', 'rails-api')
|
179
|
+
config.verikloak.leeway = Integer(ENV.fetch('VERIKLOAK_LEEWAY', '60'))
|
180
|
+
config.verikloak.skip_paths = %w[/up /health /rails/health]
|
181
|
+
|
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
|
+
config.verikloak.logger_tags = %i[request_id sub]
|
190
|
+
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
|
+
|
193
|
+
# Optional Pundit rescue (403 JSON)
|
194
|
+
config.verikloak.rescue_pundit = ENV.fetch('VERIKLOAK_RESCUE_PUNDIT', 'true') == 'true'
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### ENV Mapping
|
199
|
+
|
200
|
+
| Key | ENV var |
|
201
|
+
| --- | --- |
|
202
|
+
| `discovery_url` | `KEYCLOAK_DISCOVERY_URL` |
|
203
|
+
| `audience` | `VERIKLOAK_AUDIENCE` |
|
204
|
+
| `issuer` | `VERIKLOAK_ISSUER` |
|
205
|
+
| `leeway` | `VERIKLOAK_LEEWAY` |
|
206
|
+
| `trust_forwarded_access_token` | `VERIKLOAK_TRUST_FWD_TOKEN` |
|
207
|
+
| `render_500_json` | `VERIKLOAK_RENDER_500` |
|
208
|
+
| `rescue_pundit` | `VERIKLOAK_RESCUE_PUNDIT` |
|
209
|
+
|
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
|
+
|
215
|
+
## Errors
|
216
|
+
This gem standardizes JSON error responses and HTTP statuses. See [ERRORS.md](ERRORS.md) for details and examples.
|
217
|
+
|
218
|
+
### Statuses
|
219
|
+
| Status | Typical code(s) | When | Headers | Body (example) |
|
220
|
+
| --- | --- | --- | --- | --- |
|
221
|
+
| 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
|
+
| 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": "..." }` |
|
224
|
+
|
225
|
+
### Customize
|
226
|
+
Customize rendering by assigning `config.verikloak.error_renderer`.
|
227
|
+
|
228
|
+
Example: return a compact JSON shape while preserving `WWW-Authenticate` for 401.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
class CompactErrorRenderer
|
232
|
+
def render(controller, error)
|
233
|
+
code = error.respond_to?(:code) ? error.code : 'unauthorized'
|
234
|
+
message = error.message.to_s
|
235
|
+
|
236
|
+
status = case code
|
237
|
+
when 'forbidden' then 403
|
238
|
+
when 'jwks_fetch_failed', 'jwks_parse_failed', 'discovery_metadata_fetch_failed', 'discovery_metadata_invalid' then 503
|
239
|
+
else 401
|
240
|
+
end
|
241
|
+
|
242
|
+
if status == 401
|
243
|
+
hdr = +'Bearer'
|
244
|
+
hdr << %( error="#{sanitize_quoted(code)}") if code
|
245
|
+
hdr << %( error_description="#{sanitize_quoted(message)}") if message && !message.empty?
|
246
|
+
controller.response.set_header('WWW-Authenticate', hdr)
|
247
|
+
end
|
248
|
+
|
249
|
+
controller.render json: { code: code, msg: message }, status: status
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
Rails.application.configure do
|
254
|
+
config.verikloak.error_renderer = CompactErrorRenderer.new
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
Note: Always sanitize values placed into `WWW-Authenticate` header parameters to avoid header injection. For example:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
class CompactErrorRenderer
|
262
|
+
private
|
263
|
+
def sanitize_quoted(val)
|
264
|
+
# Escape quotes/backslashes and strip CR/LF
|
265
|
+
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]/, ' ')
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
## Optional Pundit Rescue
|
271
|
+
If the `pundit` gem is present, `Pundit::NotAuthorizedError` is rescued to a standardized 403 JSON. This is a lightweight convenience only; deeper Pundit integration (policies, helpers) is out of scope and can live in a separate plugin.
|
272
|
+
|
273
|
+
### Toggle
|
274
|
+
Toggle with `config.verikloak.rescue_pundit` (default: true). Environment example:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
# config/initializers/verikloak.rb
|
278
|
+
Rails.application.configure do
|
279
|
+
# Disable the built-in rescue if you handle Pundit errors yourself
|
280
|
+
config.verikloak.rescue_pundit = ENV.fetch('VERIKLOAK_RESCUE_PUNDIT', 'true') == 'true'
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
### Behavior Example
|
285
|
+
```ruby
|
286
|
+
# When Pundit raises:
|
287
|
+
raise Pundit::NotAuthorizedError, 'forbidden'
|
288
|
+
# The concern rescues and renders:
|
289
|
+
# { error: 'forbidden', message: 'forbidden' } with status 403
|
290
|
+
```
|
291
|
+
|
292
|
+
## Rails 8.0/8.1 Timezone Note
|
293
|
+
Rails 8.0 shows a deprecation for the upcoming 8.1 change where `to_time` preserves the receiver timezone. This gem does not call `to_time`, but your app may. To opt in and silence the deprecation, set:
|
294
|
+
```ruby
|
295
|
+
# config/application.rb
|
296
|
+
module YourApp
|
297
|
+
class Application < Rails::Application
|
298
|
+
config.active_support.to_time_preserves_timezone = :zone
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
## Development (for contributors)
|
304
|
+
Clone and install dependencies:
|
305
|
+
|
306
|
+
```bash
|
307
|
+
git clone https://github.com/taiyaky/verikloak-rails.git
|
308
|
+
cd verikloak-rails
|
309
|
+
bundle install
|
310
|
+
```
|
311
|
+
See **Testing** below to run specs and RuboCop. For releasing, see **Publishing**.
|
312
|
+
|
313
|
+
## Testing
|
314
|
+
All pull requests and pushes are automatically tested with [RSpec](https://rspec.info/) and [RuboCop](https://rubocop.org/) via GitHub Actions.
|
315
|
+
See the CI badge at the top for current build status.
|
316
|
+
|
317
|
+
To run the test suite locally:
|
318
|
+
|
319
|
+
```bash
|
320
|
+
docker compose run --rm dev rspec
|
321
|
+
docker compose run --rm dev rubocop -a
|
322
|
+
```
|
323
|
+
|
324
|
+
## Contributing
|
325
|
+
Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
326
|
+
|
327
|
+
## Security
|
328
|
+
If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
|
329
|
+
|
330
|
+
## License
|
331
|
+
This project is licensed under the [MIT License](LICENSE).
|
332
|
+
|
333
|
+
## Publishing (for maintainers)
|
334
|
+
Gem release instructions are documented separately in [MAINTAINERS.md](MAINTAINERS.md).
|
335
|
+
|
336
|
+
## Changelog
|
337
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
338
|
+
|
339
|
+
## References
|
340
|
+
- verikloak-rails (this gem): https://rubygems.org/gems/verikloak-rails
|
341
|
+
- Verikloak (base gem): https://github.com/taiyaky/verikloak
|
342
|
+
- Verikloak on RubyGems: https://rubygems.org/gems/verikloak
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Verikloak
|
6
|
+
module Generators
|
7
|
+
# Rails generator that creates `config/initializers/verikloak.rb` and prints
|
8
|
+
# follow-up instructions for configuring verikloak-rails.
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
11
|
+
|
12
|
+
desc 'Creates an initializer for verikloak-rails and documents basic usage.'
|
13
|
+
|
14
|
+
# Create the initializer file under config/initializers.
|
15
|
+
# @return [void]
|
16
|
+
# @example
|
17
|
+
# rails g verikloak:install
|
18
|
+
def create_initializer
|
19
|
+
template 'initializer.rb.erb', 'config/initializers/verikloak.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Print next steps for configuring the gem.
|
23
|
+
# @return [void]
|
24
|
+
def say_next_steps
|
25
|
+
say <<~MSG
|
26
|
+
✅ verikloak: initializer created.
|
27
|
+
|
28
|
+
Next steps:
|
29
|
+
1) Ensure the base gem is installed: gem 'verikloak', '>= 0.1.2', '< 0.2'
|
30
|
+
2) Set discovery_url / audience in config/initializers/verikloak.rb
|
31
|
+
3) (Optional) If you disable auto-include, add this line to ApplicationController:
|
32
|
+
include Verikloak::Rails::Controller
|
33
|
+
MSG
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
module Verikloak
|
6
|
+
module Rails
|
7
|
+
# Configuration for verikloak-rails.
|
8
|
+
#
|
9
|
+
# Controls how the Rack middleware is initialized (discovery, audience,
|
10
|
+
# issuer, leeway, skip paths) and Rails-specific behavior such as
|
11
|
+
# controller inclusion, logging tags, and error rendering.
|
12
|
+
#
|
13
|
+
# @!attribute [rw] discovery_url
|
14
|
+
# OIDC discovery document URL.
|
15
|
+
# @return [String, nil]
|
16
|
+
# @!attribute [rw] audience
|
17
|
+
# Expected audience (`aud`) claim. Accepts String or Array.
|
18
|
+
# @return [String, Array<String>, nil]
|
19
|
+
# @!attribute [rw] issuer
|
20
|
+
# Expected issuer (`iss`) claim.
|
21
|
+
# @return [String, nil]
|
22
|
+
# @!attribute [rw] leeway
|
23
|
+
# Clock skew allowance in seconds.
|
24
|
+
# @return [Integer]
|
25
|
+
# @!attribute [rw] skip_paths
|
26
|
+
# Paths to skip verification.
|
27
|
+
# @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
|
+
# @!attribute [rw] logger_tags
|
35
|
+
# Log tags to include (supports :request_id, :sub).
|
36
|
+
# @return [Array<Symbol>]
|
37
|
+
# @!attribute [rw] error_renderer
|
38
|
+
# Custom error renderer object responding to `render(controller, error)`.
|
39
|
+
# @return [Object]
|
40
|
+
# @!attribute [rw] auto_include_controller
|
41
|
+
# Auto-include the controller concern into ActionController::Base.
|
42
|
+
# @return [Boolean]
|
43
|
+
# @!attribute [rw] render_500_json
|
44
|
+
# Rescue StandardError and render a JSON 500 response.
|
45
|
+
# @return [Boolean]
|
46
|
+
# @!attribute [rw] token_header_priority
|
47
|
+
# Env header keys in priority order for sourcing bearer token.
|
48
|
+
# @return [Array<String>]
|
49
|
+
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
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@discovery_url = nil
|
57
|
+
@audience = nil
|
58
|
+
@issuer = nil
|
59
|
+
@leeway = 60
|
60
|
+
@skip_paths = ['/up', '/health', '/rails/health']
|
61
|
+
@trust_forwarded_access_token = false
|
62
|
+
@trusted_proxy_subnets = []
|
63
|
+
@logger_tags = %i[request_id sub]
|
64
|
+
@error_renderer = Verikloak::Rails::ErrorRenderer.new
|
65
|
+
@auto_include_controller = true
|
66
|
+
@render_500_json = false
|
67
|
+
@token_header_priority = %w[HTTP_X_FORWARDED_ACCESS_TOKEN HTTP_AUTHORIZATION]
|
68
|
+
@rescue_pundit = true
|
69
|
+
end
|
70
|
+
|
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
|
+
# Options forwarded to the base Verikloak Rack middleware.
|
78
|
+
# @return [Hash]
|
79
|
+
# @example
|
80
|
+
# Verikloak::Rails.config.middleware_options
|
81
|
+
# #=> { discovery_url: 'https://example/.well-known/openid-configuration', leeway: 60, ... }
|
82
|
+
def middleware_options
|
83
|
+
{
|
84
|
+
discovery_url: discovery_url,
|
85
|
+
audience: audience,
|
86
|
+
issuer: issuer,
|
87
|
+
leeway: leeway,
|
88
|
+
skip_paths: skip_paths
|
89
|
+
}.compact
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Verikloak
|
6
|
+
module Rails
|
7
|
+
# Controller concern providing Verikloak helpers and JSON error handling.
|
8
|
+
#
|
9
|
+
# Includes `before_action :authenticate_user!`, helpers such as
|
10
|
+
# `current_user_claims`, and consistent 401/403 responses. Optionally wraps
|
11
|
+
# requests with tagged logging and a 500 JSON renderer.
|
12
|
+
module Controller
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
before_action :authenticate_user!
|
17
|
+
# Register generic error handler first so specific handlers take precedence.
|
18
|
+
if Verikloak::Rails.config.render_500_json
|
19
|
+
rescue_from StandardError do |_e|
|
20
|
+
render json: { error: 'internal_server_error', message: 'An unexpected error occurred' },
|
21
|
+
status: :internal_server_error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
if defined?(::Pundit::NotAuthorizedError) && Verikloak::Rails.config.rescue_pundit
|
25
|
+
rescue_from ::Pundit::NotAuthorizedError do |e|
|
26
|
+
render json: { error: 'forbidden', message: e.message }, status: :forbidden
|
27
|
+
end
|
28
|
+
end
|
29
|
+
rescue_from ::Verikloak::Error do |e|
|
30
|
+
Verikloak::Rails.config.error_renderer.render(self, e)
|
31
|
+
end
|
32
|
+
around_action :_verikloak_tag_logs
|
33
|
+
end
|
34
|
+
|
35
|
+
# Ensures a user is authenticated, otherwise renders a JSON 401 response.
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
# @example In a controller
|
39
|
+
# class ApiController < ApplicationController
|
40
|
+
# before_action :authenticate_user!
|
41
|
+
# end
|
42
|
+
def authenticate_user!
|
43
|
+
return if authenticated?
|
44
|
+
|
45
|
+
e = begin
|
46
|
+
::Verikloak::Error.new('unauthorized')
|
47
|
+
rescue StandardError
|
48
|
+
StandardError.new('Unauthorized')
|
49
|
+
end
|
50
|
+
Verikloak::Rails.config.error_renderer.render(self, e)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Whether the request has verified user claims.
|
54
|
+
# @return [Boolean]
|
55
|
+
def authenticated? = current_user_claims.present?
|
56
|
+
|
57
|
+
# The verified JWT claims for the current user.
|
58
|
+
# Prefer Rack env; fall back to RequestStore when available.
|
59
|
+
# @return [Hash, nil]
|
60
|
+
def current_user_claims
|
61
|
+
env_claims = request.env['verikloak.user']
|
62
|
+
return env_claims unless env_claims.nil?
|
63
|
+
return ::RequestStore.store[:verikloak_user] if defined?(::RequestStore) && ::RequestStore.respond_to?(:store)
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# The raw bearer token used for the current request.
|
69
|
+
# Prefer Rack env; fall back to RequestStore when available.
|
70
|
+
# @return [String, nil]
|
71
|
+
def current_token
|
72
|
+
env_token = request.env['verikloak.token']
|
73
|
+
return env_token unless env_token.nil?
|
74
|
+
return ::RequestStore.store[:verikloak_token] if defined?(::RequestStore) && ::RequestStore.respond_to?(:store)
|
75
|
+
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
# The `sub` (subject) claim from the current user claims.
|
80
|
+
# @return [String, nil]
|
81
|
+
def current_subject = current_user_claims && current_user_claims['sub']
|
82
|
+
|
83
|
+
# Enforces that the current user has all required audiences.
|
84
|
+
#
|
85
|
+
# @param required [Array<String>] one or more audiences to require
|
86
|
+
# @return [void]
|
87
|
+
# @example
|
88
|
+
# with_required_audience!('my-api', 'payments')
|
89
|
+
def with_required_audience!(*required)
|
90
|
+
aud = Array(current_user_claims&.dig('aud'))
|
91
|
+
return if required.flatten.all? { |r| aud.include?(r) }
|
92
|
+
|
93
|
+
render json: { error: 'forbidden', message: 'Required audience not satisfied' }, status: :forbidden
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Wraps the request in tagged logs for request ID and subject when available.
|
99
|
+
# @yieldreturn [Object] result of the block
|
100
|
+
# @return [Object]
|
101
|
+
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
|
108
|
+
if ::Rails.logger.respond_to?(:tagged) && tags.any?
|
109
|
+
::Rails.logger.tagged(*tags, &)
|
110
|
+
else
|
111
|
+
yield
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verikloak
|
4
|
+
module Rails
|
5
|
+
# Renders JSON errors for authentication/authorization failures.
|
6
|
+
#
|
7
|
+
# When status is 401, adds a `WWW-Authenticate: Bearer` header including
|
8
|
+
# `error` and `error_description` fields when available.
|
9
|
+
class ErrorRenderer
|
10
|
+
DEFAULT_STATUS_MAP = {
|
11
|
+
'invalid_token' => 401,
|
12
|
+
'unauthorized' => 401,
|
13
|
+
'forbidden' => 403,
|
14
|
+
'jwks_fetch_failed' => 503,
|
15
|
+
'jwks_parse_failed' => 503,
|
16
|
+
'discovery_metadata_fetch_failed' => 503,
|
17
|
+
'discovery_metadata_invalid' => 503
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# Render an error as JSON, adding `WWW-Authenticate` when appropriate.
|
21
|
+
#
|
22
|
+
# @param controller [#response,#render] a Rails controller instance
|
23
|
+
# @param error [Exception] the error to render
|
24
|
+
# @return [void]
|
25
|
+
# @example
|
26
|
+
# begin
|
27
|
+
# do_auth!
|
28
|
+
# rescue Verikloak::Error => e
|
29
|
+
# Verikloak::Rails.config.error_renderer.render(self, e)
|
30
|
+
# end
|
31
|
+
def render(controller, error)
|
32
|
+
code, message = extract_code_message(error)
|
33
|
+
status = status_for(error, code)
|
34
|
+
headers = {}
|
35
|
+
if status == 401
|
36
|
+
hdr = +'Bearer'
|
37
|
+
hdr << %( error="#{sanitize_quoted(code)}") if code
|
38
|
+
hdr << %( error_description="#{sanitize_quoted(message)}") if message
|
39
|
+
headers['WWW-Authenticate'] = hdr
|
40
|
+
end
|
41
|
+
headers.each { |k, v| controller.response.set_header(k, v) }
|
42
|
+
controller.render json: { error: code || 'unauthorized', message: message }, status: status
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Extract error code and message from a given exception.
|
48
|
+
# @param error [Exception]
|
49
|
+
# @return [Array<(String, String)>]
|
50
|
+
def extract_code_message(error)
|
51
|
+
code = if error.respond_to?(:code) && error.code
|
52
|
+
error.code
|
53
|
+
else
|
54
|
+
error.class.name.split('::').last.gsub(/Error$/, '').downcase
|
55
|
+
end
|
56
|
+
[code, error.message.to_s]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Map an error to an HTTP status code.
|
60
|
+
# @param error [Exception]
|
61
|
+
# @param code [String, nil]
|
62
|
+
# @return [Integer]
|
63
|
+
def status_for(error, code)
|
64
|
+
if error.is_a?(::Verikloak::Error)
|
65
|
+
DEFAULT_STATUS_MAP[code] || 401
|
66
|
+
else
|
67
|
+
401
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sanitize a value for inclusion inside a quoted HTTP header parameter.
|
72
|
+
# Escapes quotes and backslashes, and strips CR/LF to prevent header injection.
|
73
|
+
# Why block replacement? String replacements like '\\1' are parsed as
|
74
|
+
# backreferences/escapes in Ruby, making precise escaping error‑prone.
|
75
|
+
# The block receives the literal match and we return it prefixed with a
|
76
|
+
# backslash, guaranteeing predictable escaping for both " and \\.
|
77
|
+
# @param val [String]
|
78
|
+
# @return [String]
|
79
|
+
def sanitize_quoted(val)
|
80
|
+
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]/, ' ')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,107 @@
|
|
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
|
+
# Initialize the middleware.
|
16
|
+
#
|
17
|
+
# @param app [#call] next Rack app
|
18
|
+
# @param trust_forwarded [Boolean] whether to trust forwarded access tokens
|
19
|
+
# @param trusted_proxies [Array<IPAddr>, Array<String>] subnets considered trusted
|
20
|
+
# @param header_priority [Array<String>] env header keys to search, in order
|
21
|
+
def initialize(app, trust_forwarded:, trusted_proxies:,
|
22
|
+
header_priority: %w[HTTP_X_FORWARDED_ACCESS_TOKEN HTTP_AUTHORIZATION])
|
23
|
+
@app = app
|
24
|
+
@trust_forwarded = trust_forwarded
|
25
|
+
@trusted_proxies = Array(trusted_proxies)
|
26
|
+
@header_priority = header_priority
|
27
|
+
end
|
28
|
+
|
29
|
+
# Rack entry point: possibly promote a token and pass to the next app.
|
30
|
+
#
|
31
|
+
# @param env [Hash] Rack environment
|
32
|
+
# @return [Array(Integer, Hash, #each)] Rack response triple from downstream app
|
33
|
+
# @example Promote forwarded token
|
34
|
+
# env = { 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_ACCESS_TOKEN' => 'abc' }
|
35
|
+
# status, headers, body = middleware.call(env)
|
36
|
+
def call(env)
|
37
|
+
promote_forwarded_if_trusted(env)
|
38
|
+
first = resolve_first_token_header(env)
|
39
|
+
set_authorization_from(env, first) if first
|
40
|
+
@app.call(env)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Promote X-Forwarded-Access-Token to Authorization when trusted.
|
46
|
+
# @param env [Hash]
|
47
|
+
# @return [void]
|
48
|
+
def promote_forwarded_if_trusted(env)
|
49
|
+
return unless @trust_forwarded && from_trusted_proxy?(env)
|
50
|
+
|
51
|
+
forwarded = env['HTTP_X_FORWARDED_ACCESS_TOKEN']
|
52
|
+
return if forwarded.to_s.empty?
|
53
|
+
return unless env['HTTP_AUTHORIZATION'].to_s.empty?
|
54
|
+
|
55
|
+
env['HTTP_AUTHORIZATION'] = ensure_bearer(forwarded)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Resolve the first header key from which to source a bearer token.
|
59
|
+
# Respects trust policy for forwarded tokens and never returns
|
60
|
+
# 'HTTP_AUTHORIZATION'.
|
61
|
+
#
|
62
|
+
# @param env [Hash]
|
63
|
+
# @return [String, nil]
|
64
|
+
def resolve_first_token_header(env)
|
65
|
+
candidates = @header_priority.dup
|
66
|
+
candidates -= ['HTTP_X_FORWARDED_ACCESS_TOKEN'] unless @trust_forwarded && from_trusted_proxy?(env)
|
67
|
+
candidates.find { |k| (val = env[k]) && !val.to_s.empty? && k != 'HTTP_AUTHORIZATION' }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set Authorization header from the given env header key.
|
71
|
+
# @param env [Hash]
|
72
|
+
# @param header_key [String]
|
73
|
+
# @return [void]
|
74
|
+
def set_authorization_from(env, header_key)
|
75
|
+
token = env[header_key]
|
76
|
+
env['HTTP_AUTHORIZATION'] ||= ensure_bearer(token)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Ensure the token string is prefixed with 'Bearer '.
|
80
|
+
# @param token [String]
|
81
|
+
# @return [String]
|
82
|
+
def ensure_bearer(token)
|
83
|
+
token.start_with?('Bearer') ? token : "Bearer #{token}"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Whether the request originates from a trusted proxy subnet.
|
87
|
+
# @param env [Hash]
|
88
|
+
# @return [Boolean]
|
89
|
+
def from_trusted_proxy?(env)
|
90
|
+
return true if @trusted_proxies.empty?
|
91
|
+
|
92
|
+
# Prefer REMOTE_ADDR (direct peer). Fallback to the nearest proxy from X-Forwarded-For if present.
|
93
|
+
ip = (env['REMOTE_ADDR'] || '').to_s.strip
|
94
|
+
ip = env['HTTP_X_FORWARDED_FOR'].to_s.split(',').last.to_s.strip if ip.empty? && env['HTTP_X_FORWARDED_FOR']
|
95
|
+
return false if ip.empty?
|
96
|
+
|
97
|
+
begin
|
98
|
+
req_ip = IPAddr.new(ip)
|
99
|
+
@trusted_proxies.any? { |subnet| subnet.include?(req_ip) }
|
100
|
+
rescue IPAddr::InvalidAddressError
|
101
|
+
false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
require 'verikloak/middleware'
|
5
|
+
|
6
|
+
module Verikloak
|
7
|
+
module Rails
|
8
|
+
# Hooks verikloak-rails into a Rails application lifecycle.
|
9
|
+
#
|
10
|
+
# - Applies configuration from `config.verikloak`
|
11
|
+
# - Inserts middleware (`ForwardedAccessToken`, then `Verikloak::Middleware`)
|
12
|
+
# - Auto-includes controller concern when enabled
|
13
|
+
class Railtie < ::Rails::Railtie
|
14
|
+
config.verikloak = ActiveSupport::OrderedOptions.new
|
15
|
+
|
16
|
+
# Apply configuration and insert middleware.
|
17
|
+
# @return [void]
|
18
|
+
initializer 'verikloak.configure' do |app|
|
19
|
+
Verikloak::Rails.configure do |c|
|
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|
|
24
|
+
c.send("#{key}=", rails_cfg[key]) if rails_cfg.key?(key)
|
25
|
+
end
|
26
|
+
end
|
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
|
+
::Verikloak::Middleware,
|
34
|
+
**Verikloak::Rails.config.middleware_options
|
35
|
+
end
|
36
|
+
|
37
|
+
# Optionally include the controller concern when ActionController loads.
|
38
|
+
# @return [void]
|
39
|
+
initializer 'verikloak.controller' do |_app|
|
40
|
+
ActiveSupport.on_load(:action_controller_base) do
|
41
|
+
include Verikloak::Rails::Controller if Verikloak::Rails.config.auto_include_controller
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'verikloak/rails/version'
|
4
|
+
require 'verikloak/rails/configuration'
|
5
|
+
require 'verikloak/rails/error_renderer'
|
6
|
+
require 'verikloak/rails/controller'
|
7
|
+
require 'verikloak/rails/middleware_integration'
|
8
|
+
require 'verikloak/rails/railtie'
|
9
|
+
|
10
|
+
module Verikloak
|
11
|
+
# Rails integration surface for Verikloak.
|
12
|
+
#
|
13
|
+
# Exposes configuration and Railtie hooks to wire middleware and controller
|
14
|
+
# helpers into a Rails application.
|
15
|
+
module Rails
|
16
|
+
class << self
|
17
|
+
# Global configuration object for verikloak-rails.
|
18
|
+
#
|
19
|
+
# @return [Verikloak::Rails::Configuration]
|
20
|
+
# @example Read current leeway
|
21
|
+
# Verikloak::Rails.config.leeway #=> 60
|
22
|
+
def config
|
23
|
+
@config ||= Configuration.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Configure verikloak-rails.
|
27
|
+
#
|
28
|
+
# @yieldparam [Verikloak::Rails::Configuration] config
|
29
|
+
# @return [void]
|
30
|
+
# @example
|
31
|
+
# Verikloak::Rails.configure do |c|
|
32
|
+
# c.discovery_url = ENV['KEYCLOAK_DISCOVERY_URL']
|
33
|
+
# c.audience = 'rails-api'
|
34
|
+
# c.leeway = 30
|
35
|
+
# end
|
36
|
+
def configure
|
37
|
+
yield(config)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: verikloak-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- taiyaky
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '6.1'
|
19
|
+
- - "<"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '9.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '6.1'
|
29
|
+
- - "<"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '9.0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: verikloak
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 0.1.2
|
39
|
+
- - "<"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.2'
|
42
|
+
type: :runtime
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 0.1.2
|
49
|
+
- - "<"
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0.2'
|
52
|
+
description: 'Rails integration for Verikloak: auto middleware, helpers, JSON errors,
|
53
|
+
BFF header support.'
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- CHANGELOG.md
|
59
|
+
- LICENSE
|
60
|
+
- README.md
|
61
|
+
- lib/generators/verikloak/install/install_generator.rb
|
62
|
+
- lib/verikloak-rails.rb
|
63
|
+
- lib/verikloak/rails.rb
|
64
|
+
- lib/verikloak/rails/configuration.rb
|
65
|
+
- lib/verikloak/rails/controller.rb
|
66
|
+
- lib/verikloak/rails/error_renderer.rb
|
67
|
+
- lib/verikloak/rails/middleware_integration.rb
|
68
|
+
- lib/verikloak/rails/railtie.rb
|
69
|
+
- lib/verikloak/rails/version.rb
|
70
|
+
homepage: https://github.com/taiyaky/verikloak-rails
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
metadata:
|
74
|
+
source_code_uri: https://github.com/taiyaky/verikloak-rails
|
75
|
+
changelog_uri: https://github.com/taiyaky/verikloak-rails/blob/main/CHANGELOG.md
|
76
|
+
bug_tracker_uri: https://github.com/taiyaky/verikloak-rails/issues
|
77
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-rails/0.1.0
|
78
|
+
rubygems_mfa_required: 'true'
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '3.1'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.6.9
|
94
|
+
specification_version: 4
|
95
|
+
summary: Rails integration for Verikloak (Keycloak JWT via Rack middleware)
|
96
|
+
test_files: []
|