verikloak 0.1.4 → 0.1.5
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 +14 -0
- data/README.md +46 -50
- data/lib/verikloak/discovery.rb +4 -2
- data/lib/verikloak/http.rb +38 -0
- data/lib/verikloak/jwks_cache.rb +5 -1
- data/lib/verikloak/middleware.rb +88 -15
- data/lib/verikloak/version.rb +1 -1
- data/lib/verikloak.rb +1 -0
- metadata +23 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41b02d67b2d6182f8af59436549e9d68cccf08559ecdee0539793ee3baef77a7
|
|
4
|
+
data.tar.gz: 5cd807c55d6635370149cf0090644698f470d15c9e01dc78a9332adb6659e2f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eba3a601c8c67080326955bd0557cfcc8d8098469694ef42ffac590b10a91c48cf4c3cb7250645d6db39e8208a8a05bd0bfa8b59cbdb9ced6e57e55241aa4da1
|
|
7
|
+
data.tar.gz: 553050d22b04de79bac27c00358f2c1a0779d380556297ffa2ca7bb5fa1944fe9ee55f9450ccc52cf004c7899c880484457b703d149404e80097079fff303341
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.1.5] - 2025-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Specs for `Verikloak::HTTP.default_connection`, ensuring retry middleware and timeout defaults stay in sync.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Middleware audience callables now handle zero-arity and BasicObject-style implementations without relying on `method(:call)`.
|
|
17
|
+
- README documents the shared `Verikloak::HTTP.default_connection` helper for reuse/customization.
|
|
18
|
+
|
|
19
|
+
### Dependencies
|
|
20
|
+
- Declare `faraday-retry` as a runtime dependency so the default HTTP connection can load the retry middleware.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
10
24
|
## [0.1.4] - 2025-09-20
|
|
11
25
|
|
|
12
26
|
### Chore
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# verikloak
|
|
2
2
|
|
|
3
3
|
[](https://github.com/taiyaky/verikloak/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/verikloak)
|
|
@@ -9,8 +9,6 @@ A lightweight Rack middleware for verifying Keycloak JWT access tokens via OpenI
|
|
|
9
9
|
|
|
10
10
|
Verikloak is a plug-and-play solution for Ruby (especially Rails API) apps that need to validate incoming `Bearer` tokens issued by Keycloak. It uses OpenID Connect Discovery and JWKs to fetch the public keys and verify JWT signatures securely.
|
|
11
11
|
|
|
12
|
-
---
|
|
13
|
-
|
|
14
12
|
## Features
|
|
15
13
|
|
|
16
14
|
- OpenID Connect Discovery (`.well-known/openid-configuration`)
|
|
@@ -20,8 +18,6 @@ Verikloak is a plug-and-play solution for Ruby (especially Rails API) apps that
|
|
|
20
18
|
- Rails/Rack middleware support
|
|
21
19
|
- Faraday-based customizable HTTP layer
|
|
22
20
|
|
|
23
|
-
---
|
|
24
|
-
|
|
25
21
|
## Installation
|
|
26
22
|
|
|
27
23
|
Add this line to your application's `Gemfile`:
|
|
@@ -36,8 +32,6 @@ Then install:
|
|
|
36
32
|
bundle install
|
|
37
33
|
```
|
|
38
34
|
|
|
39
|
-
---
|
|
40
|
-
|
|
41
35
|
## Usage
|
|
42
36
|
|
|
43
37
|
### Rails (API mode)
|
|
@@ -81,7 +75,6 @@ structured error responses.
|
|
|
81
75
|
> **Note:** When the Rack middleware is enabled, it already renders JSON error responses.
|
|
82
76
|
> The `rescue_from` example above is only necessary if you bypass the middleware or want custom behavior.
|
|
83
77
|
|
|
84
|
-
---
|
|
85
78
|
#### Error Hierarchy
|
|
86
79
|
|
|
87
80
|
All Verikloak errors inherit from `Verikloak::Error`:
|
|
@@ -90,7 +83,7 @@ All Verikloak errors inherit from `Verikloak::Error`:
|
|
|
90
83
|
- `Verikloak::DiscoveryError` – OIDC discovery fetch/parse (`503 Service Unavailable`)
|
|
91
84
|
- `Verikloak::JwksCacheError` – JWKs fetch/parse/cache (`503 Service Unavailable`)
|
|
92
85
|
- `Verikloak::MiddlewareError` – header/infra issues surfaced by the middleware (usually `401`, sometimes `503`)
|
|
93
|
-
|
|
86
|
+
|
|
94
87
|
#### Recommended: use environment variables in production
|
|
95
88
|
|
|
96
89
|
```ruby
|
|
@@ -100,7 +93,7 @@ config.middleware.use Verikloak::Middleware,
|
|
|
100
93
|
skip_paths: ['/', '/health', '/public/*', '/rails/*']
|
|
101
94
|
```
|
|
102
95
|
This makes the configuration secure and flexible across environments.
|
|
103
|
-
|
|
96
|
+
|
|
104
97
|
### Accessing claims in controllers
|
|
105
98
|
|
|
106
99
|
Once the middleware is enabled, Verikloak adds the decoded token and raw JWT to the Rack environment.
|
|
@@ -117,7 +110,7 @@ class Api::V1::NotesController < ApplicationController
|
|
|
117
110
|
end
|
|
118
111
|
end
|
|
119
112
|
```
|
|
120
|
-
|
|
113
|
+
|
|
121
114
|
### Standalone Rack app
|
|
122
115
|
|
|
123
116
|
```ruby
|
|
@@ -133,7 +126,6 @@ run ->(env) {
|
|
|
133
126
|
[200, { "Content-Type" => "application/json" }, [user.to_json]]
|
|
134
127
|
}
|
|
135
128
|
```
|
|
136
|
-
---
|
|
137
129
|
|
|
138
130
|
## How It Works
|
|
139
131
|
|
|
@@ -149,8 +141,6 @@ run ->(env) {
|
|
|
149
141
|
- `nbf` (not before)
|
|
150
142
|
7. Makes the decoded payload available in `env["verikloak.user"]`
|
|
151
143
|
|
|
152
|
-
---
|
|
153
|
-
|
|
154
144
|
## Error Responses
|
|
155
145
|
|
|
156
146
|
Verikloak returns JSON error responses in a consistent format with structured error codes. The HTTP status code reflects the nature of the error: 401 for client-side authentication issues, 503 for server-side discovery/JWKs errors, and 500 for unexpected internal errors.
|
|
@@ -236,13 +226,13 @@ For a full list of error cases and detailed explanations, please see the [ERRORS
|
|
|
236
226
|
| Key | Required | Description |
|
|
237
227
|
| --------------- | -------- | ------------------------------------------- |
|
|
238
228
|
| `discovery_url` | Yes | Full URL to your realm's OIDC discovery doc |
|
|
239
|
-
| `audience` | Yes | Your client ID (checked against `aud`)
|
|
229
|
+
| `audience` | Yes | Your client ID (checked against `aud`). Accepts a String or callable returning a String/Array per request. |
|
|
240
230
|
| `skip_paths` | No | Array of paths or wildcards to skip authentication, e.g. `['/', '/health', '/public/*']`. **Note:** Regex patterns are not supported. |
|
|
241
231
|
| `discovery` | No | Inject custom Discovery instance (advanced/testing) |
|
|
242
232
|
| `jwks_cache` | No | Inject custom JwksCache instance (advanced/testing) |
|
|
243
233
|
| `leeway` | No | Clock skew tolerance (seconds) applied during JWT verification. Defaults to `TokenDecoder::DEFAULT_LEEWAY`. |
|
|
244
234
|
| `token_verify_options` | No | Hash of advanced JWT verification options passed through to TokenDecoder. For example: `{ verify_iat: false, leeway: 10, algorithms: ["RS256"] }`. If both `leeway:` and `token_verify_options[:leeway]` are set, the latter takes precedence. |
|
|
245
|
-
| `connection` | No | Inject a Faraday::Connection used for both Discovery and JWKs fetches.
|
|
235
|
+
| `connection` | No | Inject a Faraday::Connection used for both Discovery and JWKs fetches. Defaults to a safe connection with timeouts and retries. |
|
|
246
236
|
|
|
247
237
|
#### Option: `skip_paths`
|
|
248
238
|
|
|
@@ -264,33 +254,51 @@ Paths **not matched** by any `skip_paths` entry will require a valid JWT.
|
|
|
264
254
|
**Note:** Regex patterns are not supported. Only literal paths and `*` wildcards are allowed.
|
|
265
255
|
Internally, `*` expands to match nested paths, so patterns like `/rails/*` are valid. This differs from regex — for example, `'/rails'` alone matches only `/rails`, while `'/rails/*'` covers both `/rails` and deeper subpaths.
|
|
266
256
|
|
|
257
|
+
#### Option: `audience`
|
|
258
|
+
|
|
259
|
+
The `audience` option may be either a static String or any callable object (Proc, lambda, object responding to `#call`). When a callable is provided it receives the Rack `env` and can return a different audience for each request. This is useful when a single gateway serves multiple downstream clients:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
Verikloak::Middleware.new(app,
|
|
263
|
+
discovery_url: ENV['DISCOVERY_URL'],
|
|
264
|
+
audience: ->(env) {
|
|
265
|
+
env['PATH_INFO'].start_with?('/admin') ? 'admin-client-id' : 'public-client-id'
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The callable may also return an Array of audiences when a route is valid for multiple clients.
|
|
271
|
+
|
|
267
272
|
#### Customizing Faraday for Discovery and JWKs
|
|
268
273
|
|
|
269
|
-
Both `Discovery` and `JwksCache` accept a `Faraday::Connection`.
|
|
270
|
-
|
|
274
|
+
Both `Discovery` and `JwksCache` accept a `Faraday::Connection`.
|
|
275
|
+
Verikloak ships with a helper you can re-use anywhere:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
connection = Verikloak::HTTP.default_connection
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The default connection enables retries (via `faraday-retry`) for idempotent GET requests and applies conservative timeouts (5s request / 2s open). If you need to add extra middleware, adapters, or instrumentation, you can build on top of the defaults:
|
|
271
282
|
|
|
272
283
|
```ruby
|
|
273
|
-
connection = Faraday.new(request: { timeout:
|
|
284
|
+
connection = Faraday.new(request: { timeout: 10 }) do |f|
|
|
285
|
+
f.request :retry, Verikloak::HTTP::RETRY_OPTIONS
|
|
274
286
|
f.response :logger
|
|
287
|
+
f.adapter Faraday.default_adapter
|
|
275
288
|
end
|
|
276
289
|
|
|
277
290
|
config.middleware.use Verikloak::Middleware,
|
|
278
291
|
discovery_url: ENV["DISCOVERY_URL"],
|
|
279
292
|
audience: ENV["CLIENT_ID"],
|
|
280
|
-
|
|
281
|
-
jwks_uri: "https://example.com/realms/myrealm/protocol/openid-connect/certs",
|
|
282
|
-
connection: connection
|
|
283
|
-
)
|
|
284
|
-
```
|
|
285
|
-
This makes it easy to apply consistent Faraday settings across both discovery and JWKs fetches.
|
|
293
|
+
connection: connection
|
|
286
294
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
discovery_url: ENV["DISCOVERY_URL"],
|
|
291
|
-
audience: ENV["CLIENT_ID"],
|
|
295
|
+
# Or pass the connection through to a shared JwksCache instance
|
|
296
|
+
jwks_cache = Verikloak::JwksCache.new(
|
|
297
|
+
jwks_uri: "https://example.com/realms/myrealm/protocol/openid-connect/certs",
|
|
292
298
|
connection: connection
|
|
299
|
+
)
|
|
293
300
|
```
|
|
301
|
+
This makes it easy to keep HTTP settings consistent across discovery, JWK refreshes, and any other Verikloak components you wire together.
|
|
294
302
|
|
|
295
303
|
#### Customizing token verification (leeway and options)
|
|
296
304
|
|
|
@@ -314,8 +322,6 @@ config.middleware.use Verikloak::Middleware,
|
|
|
314
322
|
- `token_verify_options:` is passed directly to TokenDecoder (and ultimately to `JWT.decode`).
|
|
315
323
|
- If both are set, `token_verify_options[:leeway]` takes precedence.
|
|
316
324
|
|
|
317
|
-
---
|
|
318
|
-
|
|
319
325
|
#### Performance note
|
|
320
326
|
|
|
321
327
|
Internally, Verikloak caches `TokenDecoder` instances per JWKs fetch to avoid reinitializing
|
|
@@ -330,14 +336,13 @@ Verikloak consists of modular components, each with a focused responsibility:
|
|
|
330
336
|
|----------------|--------------------------------------------------------|--------------|
|
|
331
337
|
| `Middleware` | Rack-compatible entry point for token validation | Rack layer |
|
|
332
338
|
| `Discovery` | Fetches OIDC discovery metadata (`.well-known`) | Network layer|
|
|
339
|
+
| `HTTP` | Provides shared Faraday connection with retries/timeouts | Network layer|
|
|
333
340
|
| `JwksCache` | Fetches & caches JWKs public keys (with ETag) | Cache layer |
|
|
334
341
|
| `TokenDecoder` | Decodes and verifies JWTs (signature, exp, nbf, iss, aud) | Crypto layer |
|
|
335
342
|
| `Errors` | Centralized error hierarchy | Core layer |
|
|
336
343
|
|
|
337
344
|
This separation enables better testing, modular reuse, and flexibility.
|
|
338
345
|
|
|
339
|
-
---
|
|
340
|
-
|
|
341
346
|
## Development (for contributors)
|
|
342
347
|
|
|
343
348
|
Clone and install dependencies:
|
|
@@ -349,8 +354,6 @@ bundle install
|
|
|
349
354
|
```
|
|
350
355
|
See **Testing** below to run specs and RuboCop. For releasing, see **Publishing**.
|
|
351
356
|
|
|
352
|
-
---
|
|
353
|
-
|
|
354
357
|
## Testing
|
|
355
358
|
|
|
356
359
|
All pull requests and pushes are automatically tested with [RSpec](https://rspec.info/) and [RuboCop](https://rubocop.org/) via GitHub Actions.
|
|
@@ -362,40 +365,33 @@ To run the test suite locally:
|
|
|
362
365
|
docker compose run --rm dev rspec
|
|
363
366
|
docker compose run --rm dev rubocop
|
|
364
367
|
```
|
|
365
|
-
---
|
|
366
368
|
|
|
367
369
|
## Contributing
|
|
368
370
|
|
|
369
371
|
Bug reports and pull requests are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
370
372
|
|
|
371
|
-
---
|
|
372
|
-
|
|
373
373
|
## Security
|
|
374
374
|
|
|
375
375
|
If you find a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md).
|
|
376
376
|
|
|
377
|
-
---
|
|
378
|
-
|
|
379
377
|
## License
|
|
380
378
|
|
|
381
379
|
This project is licensed under the [MIT License](LICENSE).
|
|
382
380
|
|
|
383
|
-
---
|
|
384
|
-
|
|
385
381
|
## Publishing (for maintainers)
|
|
386
382
|
|
|
387
383
|
Gem release instructions are documented separately in [MAINTAINERS.md](MAINTAINERS.md).
|
|
388
384
|
|
|
389
|
-
---
|
|
390
|
-
|
|
391
385
|
## Changelog
|
|
392
386
|
|
|
393
387
|
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
394
388
|
|
|
395
|
-
---
|
|
396
|
-
|
|
397
389
|
## References
|
|
398
390
|
|
|
399
|
-
- [OpenID Connect Discovery 1.0 Spec](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
|
400
|
-
- [Keycloak Documentation: Securing Apps](https://www.keycloak.org/docs/latest/securing_apps/#openid-connect)
|
|
401
|
-
- [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)
|
|
391
|
+
- [OpenID Connect Discovery 1.0 Spec](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
|
392
|
+
- [Keycloak Documentation: Securing Apps](https://www.keycloak.org/docs/latest/securing_apps/#openid-connect)
|
|
393
|
+
- [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)
|
|
394
|
+
- [verikloak-rails on RubyGems](https://rubygems.org/gems/verikloak-rails)
|
|
395
|
+
- [verikloak-bff on RubyGems](https://rubygems.org/gems/verikloak-bff)
|
|
396
|
+
- [verikloak-pundit on RubyGems](https://rubygems.org/gems/verikloak-pundit)
|
|
397
|
+
- [verikloak-audience on RubyGems](https://rubygems.org/gems/verikloak-audience)
|
data/lib/verikloak/discovery.rb
CHANGED
|
@@ -4,6 +4,8 @@ require 'faraday'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
7
|
+
require 'verikloak/http'
|
|
8
|
+
|
|
7
9
|
module Verikloak
|
|
8
10
|
# Fetches and caches the OpenID Connect Discovery document.
|
|
9
11
|
#
|
|
@@ -39,10 +41,10 @@ module Verikloak
|
|
|
39
41
|
REQUIRED_FIELDS = %w[jwks_uri issuer].freeze
|
|
40
42
|
|
|
41
43
|
# @param discovery_url [String] The full URL to the `.well-known/openid-configuration`.
|
|
42
|
-
# @param connection [Faraday::Connection] Optional Faraday client (for DI/tests).
|
|
44
|
+
# @param connection [Faraday::Connection] Optional Faraday client (for DI/tests).
|
|
43
45
|
# @param cache_ttl [Integer] Cache TTL in seconds (default: 3600).
|
|
44
46
|
# @raise [DiscoveryError] when `discovery_url` is not a valid HTTP(S) URL
|
|
45
|
-
def initialize(discovery_url:, connection:
|
|
47
|
+
def initialize(discovery_url:, connection: Verikloak::HTTP.default_connection, cache_ttl: 3600)
|
|
46
48
|
unless discovery_url.is_a?(String) && discovery_url.strip.match?(%r{^https?://})
|
|
47
49
|
raise DiscoveryError.new('Invalid discovery URL: must be a non-empty HTTP(S) URL',
|
|
48
50
|
code: 'invalid_discovery_url')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/retry'
|
|
5
|
+
|
|
6
|
+
module Verikloak
|
|
7
|
+
# Internal HTTP helpers shared across components.
|
|
8
|
+
module HTTP
|
|
9
|
+
# Default request timeout (seconds) for outbound discovery/JWKs calls.
|
|
10
|
+
DEFAULT_TIMEOUT = 5
|
|
11
|
+
# Default open/read timeout (seconds) before establishing the HTTP connection.
|
|
12
|
+
DEFAULT_OPEN_TIMEOUT = 2
|
|
13
|
+
|
|
14
|
+
# Retry middleware configuration used for idempotent GET requests.
|
|
15
|
+
# Retries on 429/5xx with exponential backoff and jitter.
|
|
16
|
+
RETRY_OPTIONS = {
|
|
17
|
+
max: 2,
|
|
18
|
+
interval: 0.1,
|
|
19
|
+
interval_randomness: 0.2,
|
|
20
|
+
backoff_factor: 2,
|
|
21
|
+
methods: %i[get],
|
|
22
|
+
retry_statuses: [429, 500, 502, 503, 504]
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Builds a Faraday connection with conservative defaults suitable for
|
|
26
|
+
# network-bound operations (discovery and JWKs fetching).
|
|
27
|
+
#
|
|
28
|
+
# @return [Faraday::Connection]
|
|
29
|
+
def self.default_connection
|
|
30
|
+
Faraday.new do |f|
|
|
31
|
+
f.request :retry, RETRY_OPTIONS
|
|
32
|
+
f.options.timeout = DEFAULT_TIMEOUT
|
|
33
|
+
f.options.open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
34
|
+
f.adapter Faraday.default_adapter
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/verikloak/jwks_cache.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'faraday'
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
|
+
require 'verikloak/http'
|
|
7
|
+
|
|
6
8
|
module Verikloak
|
|
7
9
|
# Caches and revalidates JSON Web Key Sets (JWKs) fetched from a remote endpoint.
|
|
8
10
|
#
|
|
@@ -43,7 +45,7 @@ module Verikloak
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
@jwks_uri = jwks_uri
|
|
46
|
-
@connection = connection ||
|
|
48
|
+
@connection = connection || Verikloak::HTTP.default_connection
|
|
47
49
|
@cached_keys = nil
|
|
48
50
|
@etag = nil
|
|
49
51
|
@fetched_at = nil
|
|
@@ -59,6 +61,8 @@ module Verikloak
|
|
|
59
61
|
# @return [Array<Hash>] the cached JWKs after fetch/revalidation
|
|
60
62
|
# @raise [JwksCacheError] on HTTP failures, invalid JSON, invalid structure, or cache miss on 304
|
|
61
63
|
def fetch!
|
|
64
|
+
return @cached_keys if fresh_by_ttl?
|
|
65
|
+
|
|
62
66
|
with_error_handling do
|
|
63
67
|
# Build conditional request headers (ETag-based)
|
|
64
68
|
headers = build_conditional_headers
|
data/lib/verikloak/middleware.rb
CHANGED
|
@@ -5,6 +5,8 @@ require 'json'
|
|
|
5
5
|
require 'set'
|
|
6
6
|
require 'faraday'
|
|
7
7
|
|
|
8
|
+
require 'verikloak/http'
|
|
9
|
+
|
|
8
10
|
module Verikloak
|
|
9
11
|
# @api private
|
|
10
12
|
#
|
|
@@ -106,12 +108,12 @@ module Verikloak
|
|
|
106
108
|
|
|
107
109
|
# Returns a cached TokenDecoder instance for current inputs.
|
|
108
110
|
# Cache key uses issuer, audience, leeway, token_verify_options, and JWKs fetched_at timestamp.
|
|
109
|
-
def decoder_for
|
|
111
|
+
def decoder_for(audience)
|
|
110
112
|
keys = @jwks_cache.cached
|
|
111
113
|
fetched_at = @jwks_cache.respond_to?(:fetched_at) ? @jwks_cache.fetched_at : nil
|
|
112
114
|
cache_key = [
|
|
113
115
|
@issuer,
|
|
114
|
-
|
|
116
|
+
audience,
|
|
115
117
|
@leeway,
|
|
116
118
|
@token_verify_options,
|
|
117
119
|
fetched_at
|
|
@@ -120,7 +122,7 @@ module Verikloak
|
|
|
120
122
|
@decoder_cache[cache_key] ||= TokenDecoder.new(
|
|
121
123
|
jwks: keys,
|
|
122
124
|
issuer: @issuer,
|
|
123
|
-
audience:
|
|
125
|
+
audience: audience,
|
|
124
126
|
leeway: @leeway,
|
|
125
127
|
options: @token_verify_options
|
|
126
128
|
)
|
|
@@ -142,14 +144,16 @@ module Verikloak
|
|
|
142
144
|
# @param token [String]
|
|
143
145
|
# @return [Hash] decoded JWT claims
|
|
144
146
|
# @raise [Verikloak::Error] bubbles up verification/fetch errors for centralized handling
|
|
145
|
-
def decode_token(token)
|
|
147
|
+
def decode_token(env, token)
|
|
146
148
|
ensure_jwks_cache!
|
|
147
149
|
if @jwks_cache.cached.nil? || @jwks_cache.cached.empty?
|
|
148
150
|
raise MiddlewareError.new('JWKs cache is empty, cannot verify token', code: 'jwks_cache_miss')
|
|
149
151
|
end
|
|
150
152
|
|
|
153
|
+
audience = resolve_audience(env)
|
|
154
|
+
|
|
151
155
|
# First attempt
|
|
152
|
-
decoder = decoder_for
|
|
156
|
+
decoder = decoder_for(audience)
|
|
153
157
|
|
|
154
158
|
begin
|
|
155
159
|
decoder.decode!(token)
|
|
@@ -160,11 +164,78 @@ module Verikloak
|
|
|
160
164
|
refresh_jwks!
|
|
161
165
|
|
|
162
166
|
# Rebuild decoder with refreshed keys and try once more.
|
|
163
|
-
decoder = decoder_for
|
|
167
|
+
decoder = decoder_for(audience)
|
|
164
168
|
decoder.decode!(token)
|
|
165
169
|
end
|
|
166
170
|
end
|
|
167
171
|
|
|
172
|
+
# Resolves the expected audience for the current request.
|
|
173
|
+
#
|
|
174
|
+
# @param env [Hash] Rack environment.
|
|
175
|
+
# @return [String, Array<String>] The expected audience value.
|
|
176
|
+
# @raise [MiddlewareError] when the resolved audience is blank.
|
|
177
|
+
def resolve_audience(env)
|
|
178
|
+
source = @audience_source
|
|
179
|
+
value = if source.respond_to?(:call)
|
|
180
|
+
callable = source
|
|
181
|
+
arity = callable.respond_to?(:arity) ? callable.arity : safe_callable_arity(callable)
|
|
182
|
+
call_with_optional_env(callable, env, arity)
|
|
183
|
+
else
|
|
184
|
+
source
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.nil?
|
|
188
|
+
|
|
189
|
+
if value.is_a?(Array)
|
|
190
|
+
raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.empty?
|
|
191
|
+
|
|
192
|
+
return value
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
normalized = value.to_s
|
|
196
|
+
raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if normalized.empty?
|
|
197
|
+
|
|
198
|
+
normalized
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Invokes the audience callable, passing the Rack env only when required.
|
|
202
|
+
# Falls back to a zero-argument invocation if the callable raises
|
|
203
|
+
# `ArgumentError` due to an unexpected argument.
|
|
204
|
+
#
|
|
205
|
+
# @param callable [#call] Audience resolver callable.
|
|
206
|
+
# @param env [Hash] Rack environment.
|
|
207
|
+
# @param arity [Integer, nil] Callable arity when known, nil otherwise.
|
|
208
|
+
# @return [Object] Audience value returned by the callable.
|
|
209
|
+
# @raise [ArgumentError] when the callable raises for reasons other than arity mismatch.
|
|
210
|
+
def call_with_optional_env(callable, env, arity)
|
|
211
|
+
return callable.call if arity&.zero?
|
|
212
|
+
|
|
213
|
+
callable.call(env)
|
|
214
|
+
rescue ArgumentError => e
|
|
215
|
+
raise unless arity.nil? && wrong_arity_error?(e)
|
|
216
|
+
|
|
217
|
+
callable.call
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Safely obtains a callable's arity, returning nil when `#method(:call)`
|
|
221
|
+
# cannot be resolved (e.g., BasicObject-based objects).
|
|
222
|
+
#
|
|
223
|
+
# @param callable [#call]
|
|
224
|
+
# @return [Integer, nil]
|
|
225
|
+
def safe_callable_arity(callable)
|
|
226
|
+
callable.method(:call).arity
|
|
227
|
+
rescue NameError
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns true when the ArgumentError message indicates a wrong arity.
|
|
232
|
+
#
|
|
233
|
+
# @param error [ArgumentError]
|
|
234
|
+
# @return [Boolean]
|
|
235
|
+
def wrong_arity_error?(error)
|
|
236
|
+
error.message.include?('wrong number of arguments')
|
|
237
|
+
end
|
|
238
|
+
|
|
168
239
|
# Ensures that discovery metadata and JWKs cache are initialized and up-to-date.
|
|
169
240
|
# This method is thread-safe.
|
|
170
241
|
#
|
|
@@ -280,11 +351,13 @@ module Verikloak
|
|
|
280
351
|
|
|
281
352
|
# @param app [#call] downstream Rack app
|
|
282
353
|
# @param discovery_url [String] OIDC discovery endpoint URL
|
|
283
|
-
# @param audience [String]
|
|
354
|
+
# @param audience [String, #call] Expected `aud` claim. When a callable is provided it
|
|
355
|
+
# receives the Rack env and may return a String or Array of audiences.
|
|
284
356
|
# @param skip_paths [Array<String>] literal paths or wildcard patterns to bypass auth
|
|
285
357
|
# @param discovery [Discovery, nil] custom discovery instance (for DI/tests)
|
|
286
358
|
# @param jwks_cache [JwksCache, nil] custom JWKs cache instance (for DI/tests)
|
|
287
|
-
# @param connection [Faraday::Connection, nil] Optional injected Faraday connection
|
|
359
|
+
# @param connection [Faraday::Connection, nil] Optional injected Faraday connection
|
|
360
|
+
# (defaults to {Verikloak::HTTP.default_connection})
|
|
288
361
|
# @param leeway [Integer] Clock skew tolerance in seconds for token verification (delegated to TokenDecoder)
|
|
289
362
|
# @param token_verify_options [Hash] Additional JWT verification options passed through
|
|
290
363
|
# to TokenDecoder.
|
|
@@ -298,12 +371,12 @@ module Verikloak
|
|
|
298
371
|
connection: nil,
|
|
299
372
|
leeway: Verikloak::TokenDecoder::DEFAULT_LEEWAY,
|
|
300
373
|
token_verify_options: {})
|
|
301
|
-
@app
|
|
302
|
-
@
|
|
303
|
-
@
|
|
304
|
-
@
|
|
305
|
-
@
|
|
306
|
-
@leeway
|
|
374
|
+
@app = app
|
|
375
|
+
@connection = connection || Verikloak::HTTP.default_connection
|
|
376
|
+
@audience_source = audience
|
|
377
|
+
@discovery = discovery || Discovery.new(discovery_url: discovery_url, connection: @connection)
|
|
378
|
+
@jwks_cache = jwks_cache
|
|
379
|
+
@leeway = leeway
|
|
307
380
|
@token_verify_options = token_verify_options || {}
|
|
308
381
|
@issuer = nil
|
|
309
382
|
@mutex = Mutex.new
|
|
@@ -344,7 +417,7 @@ module Verikloak
|
|
|
344
417
|
# @param token [String]
|
|
345
418
|
# @return [Array(Integer, Hash, Array<String>)] Rack response triple
|
|
346
419
|
def handle_request(env, token)
|
|
347
|
-
claims = decode_token(token)
|
|
420
|
+
claims = decode_token(env, token)
|
|
348
421
|
env['verikloak.token'] = token
|
|
349
422
|
env['verikloak.user'] = claims
|
|
350
423
|
@app.call(env)
|
data/lib/verikloak/version.rb
CHANGED
data/lib/verikloak.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: verikloak
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -29,6 +29,26 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '3.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday-retry
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '2.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3.0'
|
|
32
52
|
- !ruby/object:Gem::Dependency
|
|
33
53
|
name: json
|
|
34
54
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -77,6 +97,7 @@ files:
|
|
|
77
97
|
- lib/verikloak.rb
|
|
78
98
|
- lib/verikloak/discovery.rb
|
|
79
99
|
- lib/verikloak/errors.rb
|
|
100
|
+
- lib/verikloak/http.rb
|
|
80
101
|
- lib/verikloak/jwks_cache.rb
|
|
81
102
|
- lib/verikloak/middleware.rb
|
|
82
103
|
- lib/verikloak/token_decoder.rb
|
|
@@ -88,7 +109,7 @@ metadata:
|
|
|
88
109
|
source_code_uri: https://github.com/taiyaky/verikloak
|
|
89
110
|
changelog_uri: https://github.com/taiyaky/verikloak/blob/main/CHANGELOG.md
|
|
90
111
|
bug_tracker_uri: https://github.com/taiyaky/verikloak/issues
|
|
91
|
-
documentation_uri: https://rubydoc.info/gems/verikloak/0.1.
|
|
112
|
+
documentation_uri: https://rubydoc.info/gems/verikloak/0.1.5
|
|
92
113
|
rubygems_mfa_required: 'true'
|
|
93
114
|
rdoc_options: []
|
|
94
115
|
require_paths:
|