verikloak 0.1.2 → 0.1.3
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 +15 -4
- data/README.md +20 -27
- data/lib/verikloak/errors.rb +2 -2
- data/lib/verikloak/jwks_cache.rb +12 -12
- data/lib/verikloak/middleware.rb +14 -14
- data/lib/verikloak/token_decoder.rb +3 -3
- data/lib/verikloak/version.rb +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fb6063573fea932d0e6a73ef18bc31b32be0d36d7c38cef34a7424bfbb3289a
|
|
4
|
+
data.tar.gz: 25cf61bedbbef60f8136bed435d640ab23993306d363db0ab2fc6fb997b08364
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82ab3fa49d2f1c82a46d3cd2573b390a744b96e35a688be413da86d715fd845de30ba5d76a83dd4c54e3e2777a1c0c24975c5005179d62a79679e5bf37651619
|
|
7
|
+
data.tar.gz: cc5a8fcd4a2679f2438390bb5cc1e0818866b162bfc2131724dae21af05fe16c59bf0c23fb70202ca0da34d7231eb720d4fec5c61a5e2405c6427a5d72a722b0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,16 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.1.3] - 2025-09-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Relax `jwt` runtime dependency to `>= 2.7, < 4.0` to allow jwt 3.x (PR #11).
|
|
14
|
+
|
|
15
|
+
### Chore
|
|
16
|
+
- Bump dev dependency `rubocop` to 1.80.2 (PR #13).
|
|
17
|
+
- Bump dev dependency `rubocop-rspec` to 3.7.0 (PR #12).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
10
21
|
## [0.1.2] - 2025-08-31
|
|
11
22
|
|
|
12
23
|
### Added
|
|
13
|
-
- Middleware: new `connection:` option to inject a Faraday::Connection, shared by Discovery and
|
|
24
|
+
- Middleware: new `connection:` option to inject a Faraday::Connection, shared by Discovery and JWKs.
|
|
14
25
|
- Middleware: new `leeway:` and `token_verify_options:` options, delegated to TokenDecoder.
|
|
15
26
|
- README: documented usage of `connection`, leeway/options, and clarified `skip_paths` behavior.
|
|
16
27
|
|
|
17
28
|
### Changed
|
|
18
29
|
- Middleware: `skip_paths` semantics clarified — plain paths are exact-match only, use `/*` for prefix matching.
|
|
19
|
-
- Middleware: TokenDecoder instances are now cached per
|
|
30
|
+
- Middleware: TokenDecoder instances are now cached per JWKs fetch for performance improvement.
|
|
20
31
|
- Internal: RuboCop style fixes (`HashExcept`, `HashTransformKeys`, long line splits).
|
|
21
32
|
|
|
22
33
|
---
|
|
@@ -38,7 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
38
49
|
- Rack middleware for verifying JWT access tokens from Keycloak
|
|
39
50
|
- Support for OpenID Connect Discovery (`.well-known/openid-configuration`)
|
|
40
51
|
- Handles up to 3 HTTP redirects and resolves relative `Location` headers
|
|
41
|
-
-
|
|
52
|
+
- JWKs fetching with in-memory caching and ETag validation
|
|
42
53
|
- RS256 JWT verification with `kid` matching
|
|
43
54
|
- Claim validation: `aud`, `iss`, `exp`, `nbf`
|
|
44
55
|
- Configurable via `discovery_url`, `audience`, and `skip_paths` options
|
|
@@ -55,7 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
66
|
- RuboCop static analysis configuration
|
|
56
67
|
- Structured error handling & responses:
|
|
57
68
|
- Token/auth errors → **401 Unauthorized** with `WWW-Authenticate` header (RFC 6750)
|
|
58
|
-
- Discovery/
|
|
69
|
+
- Discovery/JWKs errors → **503 Service Unavailable**
|
|
59
70
|
- Structured error codes: `invalid_token`, `expired_token`, `not_yet_valid`,
|
|
60
71
|
`invalid_issuer`, `invalid_audience`, `unsupported_algorithm`,
|
|
61
72
|
`jwks_fetch_failed`, `jwks_parse_failed`, `jwks_cache_miss`,
|
data/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/taiyaky/verikloak/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/verikloak)
|
|
5
|
-

|
|
6
6
|
[](https://rubygems.org/gems/verikloak)
|
|
7
7
|
|
|
8
8
|
A lightweight Rack middleware for verifying Keycloak JWT access tokens via OpenID Connect.
|
|
9
9
|
|
|
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
|
|
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
12
|
---
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
16
|
- OpenID Connect Discovery (`.well-known/openid-configuration`)
|
|
17
|
-
- JWKs auto-
|
|
17
|
+
- JWKs auto-fetching with in-memory caching and ETag support
|
|
18
18
|
- RS256 JWT verification using `kid`
|
|
19
19
|
- `aud`, `iss`, `exp`, `nbf` claim validation
|
|
20
20
|
- Rails/Rack middleware support
|
|
@@ -53,7 +53,7 @@ config.middleware.use Verikloak::Middleware,
|
|
|
53
53
|
|
|
54
54
|
#### Handling Authentication Failures
|
|
55
55
|
|
|
56
|
-
When you use the Rack middleware, authentication failures are automatically converted into JSON error responses (for example, `401` for token issues, `503` for
|
|
56
|
+
When you use the Rack middleware, authentication failures are automatically converted into JSON error responses (for example, `401` for token issues, `503` for JWKs/discovery errors). In most cases **you do not need to add custom `rescue_from` handlers** in Rails controllers.
|
|
57
57
|
|
|
58
58
|
If you use Verikloak components directly (bypassing the Rack middleware) or prefer centralized error handling, rescue from the base class `Verikloak::Error`. You can also match subclasses such as `Verikloak::TokenDecoderError`, `Verikloak::DiscoveryError`, or `Verikloak::JwksCacheError` depending on your needs:
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@ All Verikloak errors inherit from `Verikloak::Error`:
|
|
|
88
88
|
|
|
89
89
|
- `Verikloak::TokenDecoderError` – token parsing/verification (`401 Unauthorized`)
|
|
90
90
|
- `Verikloak::DiscoveryError` – OIDC discovery fetch/parse (`503 Service Unavailable`)
|
|
91
|
-
- `Verikloak::JwksCacheError` –
|
|
91
|
+
- `Verikloak::JwksCacheError` – JWKs fetch/parse/cache (`503 Service Unavailable`)
|
|
92
92
|
- `Verikloak::MiddlewareError` – header/infra issues surfaced by the middleware (usually `401`, sometimes `503`)
|
|
93
93
|
---
|
|
94
94
|
#### Recommended: use environment variables in production
|
|
@@ -99,14 +99,7 @@ config.middleware.use Verikloak::Middleware,
|
|
|
99
99
|
audience: ENV.fetch("CLIENT_ID"),
|
|
100
100
|
skip_paths: ['/', '/health', '/public/*', '/rails/*']
|
|
101
101
|
```
|
|
102
|
-
#### In production, set these variables in your environment for security and flexibility.
|
|
103
|
-
|
|
104
102
|
This makes the configuration secure and flexible across environments.
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
request.env["verikloak.user"] # => JWT claims hash
|
|
108
|
-
request.env["verikloak.token"] # => Raw JWT string
|
|
109
|
-
```
|
|
110
103
|
---
|
|
111
104
|
### Accessing claims in controllers
|
|
112
105
|
|
|
@@ -146,7 +139,7 @@ run ->(env) {
|
|
|
146
139
|
|
|
147
140
|
1. Extracts the `Authorization: Bearer <token>` header
|
|
148
141
|
2. Fetches the OIDC discovery document (only once or when expired)
|
|
149
|
-
3. Downloads
|
|
142
|
+
3. Downloads JWKs public keys from the provided `jwks_uri`
|
|
150
143
|
4. Matches the `kid` from JWT header to select the right JWK
|
|
151
144
|
5. Decodes and verifies the JWT using `RS256`
|
|
152
145
|
6. Validates the following claims:
|
|
@@ -160,12 +153,12 @@ run ->(env) {
|
|
|
160
153
|
|
|
161
154
|
## Error Responses
|
|
162
155
|
|
|
163
|
-
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/
|
|
156
|
+
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.
|
|
164
157
|
|
|
165
158
|
### Common HTTP Responses
|
|
166
159
|
|
|
167
160
|
- `401 Unauthorized`: The access token is missing, invalid, expired, or otherwise not valid.
|
|
168
|
-
- `503 Service Unavailable`: Discovery or
|
|
161
|
+
- `503 Service Unavailable`: Discovery or JWKs fetch/parsing failed (server-side issue).
|
|
169
162
|
- `500 Internal Server Error`: An unexpected error occurred.
|
|
170
163
|
|
|
171
164
|
### Representative Examples
|
|
@@ -187,14 +180,14 @@ Verikloak returns JSON error responses in a consistent format with structured er
|
|
|
187
180
|
```json
|
|
188
181
|
{
|
|
189
182
|
"error": "jwks_fetch_failed",
|
|
190
|
-
"message": "Failed to fetch
|
|
183
|
+
"message": "Failed to fetch JWKs"
|
|
191
184
|
}
|
|
192
185
|
```
|
|
193
186
|
|
|
194
187
|
```json
|
|
195
188
|
{
|
|
196
189
|
"error": "jwks_parse_failed",
|
|
197
|
-
"message": "Failed to parse
|
|
190
|
+
"message": "Failed to parse JWKs"
|
|
198
191
|
}
|
|
199
192
|
```
|
|
200
193
|
|
|
@@ -225,9 +218,9 @@ Verikloak returns JSON error responses in a consistent format with structured er
|
|
|
225
218
|
| `invalid_issuer` | 401 Unauthorized | Invalid `iss` claim |
|
|
226
219
|
| `invalid_audience` | 401 Unauthorized | Invalid `aud` claim |
|
|
227
220
|
| `not_yet_valid` | 401 Unauthorized | The token is not yet valid (`nbf` in the future) |
|
|
228
|
-
| `jwks_fetch_failed` | 503 Service Unavailable | Failed to fetch
|
|
229
|
-
| `jwks_parse_failed` | 503 Service Unavailable | Failed to parse
|
|
230
|
-
| `jwks_cache_miss` | 503 Service Unavailable |
|
|
221
|
+
| `jwks_fetch_failed` | 503 Service Unavailable | Failed to fetch JWKs |
|
|
222
|
+
| `jwks_parse_failed` | 503 Service Unavailable | Failed to parse JWKs |
|
|
223
|
+
| `jwks_cache_miss` | 503 Service Unavailable | JWKs cache is empty (e.g., 304 Not Modified without prior cache) |
|
|
231
224
|
| `discovery_metadata_fetch_failed` | 503 Service Unavailable | Failed to fetch OIDC discovery document |
|
|
232
225
|
| `discovery_metadata_invalid` | 503 Service Unavailable | Failed to parse OIDC discovery document |
|
|
233
226
|
| `discovery_redirect_error` | 503 Service Unavailable | Discovery response was a redirect without a valid Location header |
|
|
@@ -249,7 +242,7 @@ For a full list of error cases and detailed explanations, please see the [ERRORS
|
|
|
249
242
|
| `jwks_cache` | No | Inject custom JwksCache instance (advanced/testing) |
|
|
250
243
|
| `leeway` | No | Clock skew tolerance (seconds) applied during JWT verification. Defaults to `TokenDecoder::DEFAULT_LEEWAY`. |
|
|
251
244
|
| `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. |
|
|
252
|
-
| `connection` | No | Inject a Faraday::Connection used for both Discovery and
|
|
245
|
+
| `connection` | No | Inject a Faraday::Connection used for both Discovery and JWKs fetches. Allows unified timeout, retry, and headers. |
|
|
253
246
|
|
|
254
247
|
#### Option: `skip_paths`
|
|
255
248
|
|
|
@@ -271,7 +264,7 @@ Paths **not matched** by any `skip_paths` entry will require a valid JWT.
|
|
|
271
264
|
**Note:** Regex patterns are not supported. Only literal paths and `*` wildcards are allowed.
|
|
272
265
|
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.
|
|
273
266
|
|
|
274
|
-
#### Customizing Faraday for Discovery and
|
|
267
|
+
#### Customizing Faraday for Discovery and JWKs
|
|
275
268
|
|
|
276
269
|
Both `Discovery` and `JwksCache` accept a `Faraday::Connection`.
|
|
277
270
|
This allows you to configure timeouts, retries, logging, and shared headers:
|
|
@@ -289,7 +282,7 @@ config.middleware.use Verikloak::Middleware,
|
|
|
289
282
|
connection: connection
|
|
290
283
|
)
|
|
291
284
|
```
|
|
292
|
-
This makes it easy to apply consistent Faraday settings across both discovery and
|
|
285
|
+
This makes it easy to apply consistent Faraday settings across both discovery and JWKs fetches.
|
|
293
286
|
|
|
294
287
|
```ruby
|
|
295
288
|
# Alternatively, you can pass the connection directly to the middleware:
|
|
@@ -325,9 +318,9 @@ config.middleware.use Verikloak::Middleware,
|
|
|
325
318
|
|
|
326
319
|
#### Performance note
|
|
327
320
|
|
|
328
|
-
Internally, Verikloak caches `TokenDecoder` instances per
|
|
321
|
+
Internally, Verikloak caches `TokenDecoder` instances per JWKs fetch to avoid reinitializing
|
|
329
322
|
them on every request. This improves performance while still ensuring that keys are
|
|
330
|
-
revalidated when
|
|
323
|
+
revalidated when JWKs is refreshed.
|
|
331
324
|
|
|
332
325
|
## Architecture
|
|
333
326
|
|
|
@@ -337,7 +330,7 @@ Verikloak consists of modular components, each with a focused responsibility:
|
|
|
337
330
|
|----------------|--------------------------------------------------------|--------------|
|
|
338
331
|
| `Middleware` | Rack-compatible entry point for token validation | Rack layer |
|
|
339
332
|
| `Discovery` | Fetches OIDC discovery metadata (`.well-known`) | Network layer|
|
|
340
|
-
| `JwksCache` | Fetches & caches
|
|
333
|
+
| `JwksCache` | Fetches & caches JWKs public keys (with ETag) | Cache layer |
|
|
341
334
|
| `TokenDecoder` | Decodes and verifies JWTs (signature, exp, nbf, iss, aud) | Crypto layer |
|
|
342
335
|
| `Errors` | Centralized error hierarchy | Core layer |
|
|
343
336
|
|
|
@@ -405,4 +398,4 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
|
405
398
|
|
|
406
399
|
- [OpenID Connect Discovery 1.0 Spec](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
|
407
400
|
- [Keycloak Documentation: Securing Apps](https://www.keycloak.org/docs/latest/securing_apps/#openid-connect)
|
|
408
|
-
- [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)
|
|
401
|
+
- [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)
|
data/lib/verikloak/errors.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Verikloak
|
|
|
35
35
|
|
|
36
36
|
# Raised for middleware-level failures while processing a Rack request.
|
|
37
37
|
#
|
|
38
|
-
# Examples include missing/invalid Authorization headers,
|
|
38
|
+
# Examples include missing/invalid Authorization headers, JWKs cache
|
|
39
39
|
# initialization failures, or infrastructure issues detected by the
|
|
40
40
|
# middleware itself.
|
|
41
41
|
#
|
|
@@ -55,7 +55,7 @@ module Verikloak
|
|
|
55
55
|
# @raise [TokenDecoderError] from {Verikloak::TokenDecoder#decode!}
|
|
56
56
|
class TokenDecoderError < Error; end
|
|
57
57
|
|
|
58
|
-
# Raised when
|
|
58
|
+
# Raised when JWKs fetching, validation, or cache handling fails.
|
|
59
59
|
#
|
|
60
60
|
# Causes include HTTP failures, invalid JSON, missing required JWK fields,
|
|
61
61
|
# or receiving 304 Not Modified without a prior cached value.
|
data/lib/verikloak/jwks_cache.rb
CHANGED
|
@@ -4,14 +4,14 @@ require 'faraday'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
6
|
module Verikloak
|
|
7
|
-
# Caches and revalidates JSON Web Key Sets (
|
|
7
|
+
# Caches and revalidates JSON Web Key Sets (JWKs) fetched from a remote endpoint.
|
|
8
8
|
#
|
|
9
9
|
# This cache supports two HTTP cache mechanisms:
|
|
10
10
|
# - **ETag revalidation** via `If-None-Match` → returns `304 Not Modified` when unchanged.
|
|
11
11
|
# - **TTL freshness** via `Cache-Control: max-age` → avoids HTTP requests while fresh.
|
|
12
12
|
#
|
|
13
13
|
# On a successful `200 OK`, the cache:
|
|
14
|
-
# - Parses the
|
|
14
|
+
# - Parses the JWKs JSON (`{"keys":[...]}`) and validates each JWK has `kid`, `kty`, `n`, `e`.
|
|
15
15
|
# - Stores the keys in-memory, records `ETag`, and computes freshness from `Cache-Control`.
|
|
16
16
|
#
|
|
17
17
|
# On a `304 Not Modified`, the cache:
|
|
@@ -34,12 +34,12 @@ module Verikloak
|
|
|
34
34
|
# adapters, and shared headers (kept consistent with Discovery).
|
|
35
35
|
# `JwksCache.new(jwks_uri: "...", connection: Faraday.new { |f| f.request :retry })`
|
|
36
36
|
class JwksCache
|
|
37
|
-
# @param jwks_uri [String] HTTPS URL of the
|
|
37
|
+
# @param jwks_uri [String] HTTPS URL of the JWKs endpoint
|
|
38
38
|
# @param connection [Faraday::Connection, nil] Optional Faraday connection for HTTP requests
|
|
39
39
|
# @raise [JwksCacheError] if the URI is not an HTTP(S) URL
|
|
40
40
|
def initialize(jwks_uri:, connection: nil)
|
|
41
41
|
unless jwks_uri.is_a?(String) && jwks_uri.strip.match?(%r{^https?://})
|
|
42
|
-
raise JwksCacheError.new('Invalid
|
|
42
|
+
raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed')
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
@jwks_uri = jwks_uri
|
|
@@ -50,7 +50,7 @@ module Verikloak
|
|
|
50
50
|
@max_age = nil
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
# Fetches the
|
|
53
|
+
# Fetches the JWKs and updates the in-memory cache.
|
|
54
54
|
#
|
|
55
55
|
# Performs an HTTP GET with `If-None-Match` when an ETag is present and handles:
|
|
56
56
|
# - 200: parses/validates body, updates keys, ETag, TTL and `fetched_at`.
|
|
@@ -103,11 +103,11 @@ module Verikloak
|
|
|
103
103
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError
|
|
104
104
|
raise JwksCacheError.new('Connection failed', code: 'jwks_fetch_failed')
|
|
105
105
|
rescue Faraday::Error => e
|
|
106
|
-
raise JwksCacheError.new("
|
|
106
|
+
raise JwksCacheError.new("JWKs fetch failed: #{e.message}", code: 'jwks_fetch_failed')
|
|
107
107
|
rescue JSON::ParserError
|
|
108
108
|
raise JwksCacheError.new('Response is not valid JSON', code: 'jwks_parse_failed')
|
|
109
109
|
rescue StandardError => e
|
|
110
|
-
raise JwksCacheError.new("Unexpected
|
|
110
|
+
raise JwksCacheError.new("Unexpected JWKs fetch error: #{e.message}", code: 'jwks_fetch_failed')
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# @api private
|
|
@@ -161,7 +161,7 @@ module Verikloak
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
# @api private
|
|
164
|
-
# Extracts and validates the `keys` array from a
|
|
164
|
+
# Extracts and validates the `keys` array from a JWKs JSON document.
|
|
165
165
|
# Ensures each key has `kid`, `kty`, `n`, and `e`.
|
|
166
166
|
#
|
|
167
167
|
# @param json [Hash]
|
|
@@ -213,12 +213,12 @@ module Verikloak
|
|
|
213
213
|
# Revalidation succeeded; update freshness from 304 headers if present
|
|
214
214
|
process_not_modified(response)
|
|
215
215
|
else
|
|
216
|
-
raise JwksCacheError.new("Failed to fetch
|
|
216
|
+
raise JwksCacheError.new("Failed to fetch JWKs: status #{response.status}", code: 'jwks_fetch_failed')
|
|
217
217
|
end
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
# @api private
|
|
221
|
-
# Handles a 200 OK
|
|
221
|
+
# Handles a 200 OK JWKs response.
|
|
222
222
|
# @param response [Faraday::Response]
|
|
223
223
|
# @return [Array<Hash>] parsed and cached keys
|
|
224
224
|
def process_successful_response(response)
|
|
@@ -229,7 +229,7 @@ module Verikloak
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
# @api private
|
|
232
|
-
# Handles a 304 Not Modified
|
|
232
|
+
# Handles a 304 Not Modified JWKs response: updates TTL and timestamp, returns cached keys.
|
|
233
233
|
# @param response [Faraday::Response]
|
|
234
234
|
# @return [Array<Hash>]
|
|
235
235
|
# @raise [JwksCacheError] when cache is empty
|
|
@@ -246,7 +246,7 @@ module Verikloak
|
|
|
246
246
|
# @return [Array<Hash>]
|
|
247
247
|
# @raise [JwksCacheError]
|
|
248
248
|
def return_from_cache_or_fail
|
|
249
|
-
@cached_keys || raise(JwksCacheError.new('
|
|
249
|
+
@cached_keys || raise(JwksCacheError.new('JWKs cache is empty but received 304 Not Modified',
|
|
250
250
|
code: 'jwks_cache_miss'))
|
|
251
251
|
end
|
|
252
252
|
end
|
data/lib/verikloak/middleware.rb
CHANGED
|
@@ -86,12 +86,12 @@ module Verikloak
|
|
|
86
86
|
|
|
87
87
|
# @api private
|
|
88
88
|
#
|
|
89
|
-
# Internal mixin for JWT verification and discovery/
|
|
89
|
+
# Internal mixin for JWT verification and discovery/JWKs management.
|
|
90
90
|
# Extracted from Middleware to reduce class length and improve clarity.
|
|
91
91
|
module MiddlewareTokenVerification
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
# Determines whether a token verification failure warrants a one-time
|
|
94
|
+
# Determines whether a token verification failure warrants a one-time JWKs refresh
|
|
95
95
|
# and retry (e.g., after key rotation).
|
|
96
96
|
#
|
|
97
97
|
# @param error [Exception]
|
|
@@ -105,7 +105,7 @@ module Verikloak
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Returns a cached TokenDecoder instance for current inputs.
|
|
108
|
-
# Cache key uses issuer, audience, leeway, token_verify_options, and
|
|
108
|
+
# Cache key uses issuer, audience, leeway, token_verify_options, and JWKs fetched_at timestamp.
|
|
109
109
|
def decoder_for
|
|
110
110
|
keys = @jwks_cache.cached
|
|
111
111
|
fetched_at = @jwks_cache.respond_to?(:fetched_at) ? @jwks_cache.fetched_at : nil
|
|
@@ -127,7 +127,7 @@ module Verikloak
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
# Ensures
|
|
130
|
+
# Ensures JWKs are up-to-date by invoking {#ensure_jwks_cache!}.
|
|
131
131
|
# Errors are not swallowed and are handled by the caller.
|
|
132
132
|
#
|
|
133
133
|
# @return [void]
|
|
@@ -136,8 +136,8 @@ module Verikloak
|
|
|
136
136
|
ensure_jwks_cache!
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
# Decodes and verifies the JWT using the cached
|
|
140
|
-
# failures (e.g., key rotation), it refreshes the
|
|
139
|
+
# Decodes and verifies the JWT using the cached JWKs. On certain verification
|
|
140
|
+
# failures (e.g., key rotation), it refreshes the JWKs and retries once.
|
|
141
141
|
#
|
|
142
142
|
# @param token [String]
|
|
143
143
|
# @return [Hash] decoded JWT claims
|
|
@@ -145,7 +145,7 @@ module Verikloak
|
|
|
145
145
|
def decode_token(token)
|
|
146
146
|
ensure_jwks_cache!
|
|
147
147
|
if @jwks_cache.cached.nil? || @jwks_cache.cached.empty?
|
|
148
|
-
raise MiddlewareError.new('
|
|
148
|
+
raise MiddlewareError.new('JWKs cache is empty, cannot verify token', code: 'jwks_cache_miss')
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
# First attempt
|
|
@@ -154,7 +154,7 @@ module Verikloak
|
|
|
154
154
|
begin
|
|
155
155
|
decoder.decode!(token)
|
|
156
156
|
rescue TokenDecoderError => e
|
|
157
|
-
# On key rotation or signature mismatch, refresh
|
|
157
|
+
# On key rotation or signature mismatch, refresh JWKs and retry once.
|
|
158
158
|
raise unless retryable_decoder_error?(e)
|
|
159
159
|
|
|
160
160
|
refresh_jwks!
|
|
@@ -165,11 +165,11 @@ module Verikloak
|
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
# Ensures that discovery metadata and
|
|
168
|
+
# Ensures that discovery metadata and JWKs cache are initialized and up-to-date.
|
|
169
169
|
# This method is thread-safe.
|
|
170
170
|
#
|
|
171
171
|
# * When the cache instance is missing, it is created from discovery metadata.
|
|
172
|
-
# *
|
|
172
|
+
# * JWKs are (re)fetched every time; ETag/Cache-Control headers minimize traffic.
|
|
173
173
|
#
|
|
174
174
|
# @return [void]
|
|
175
175
|
# @raise [Verikloak::DiscoveryError, Verikloak::JwksCacheError, Verikloak::MiddlewareError]
|
|
@@ -188,7 +188,7 @@ module Verikloak
|
|
|
188
188
|
# Re-raise so that specific error codes can be mapped in the middleware
|
|
189
189
|
raise e
|
|
190
190
|
rescue StandardError => e
|
|
191
|
-
raise MiddlewareError.new("Failed to initialize
|
|
191
|
+
raise MiddlewareError.new("Failed to initialize JWKs cache: #{e.message}", code: 'jwks_fetch_failed')
|
|
192
192
|
end
|
|
193
193
|
end
|
|
194
194
|
|
|
@@ -267,7 +267,7 @@ module Verikloak
|
|
|
267
267
|
end
|
|
268
268
|
|
|
269
269
|
# Rack middleware that verifies incoming JWT access tokens (Keycloak) using
|
|
270
|
-
# OpenID Connect discovery and
|
|
270
|
+
# OpenID Connect discovery and JWKs. On success, it populates:
|
|
271
271
|
#
|
|
272
272
|
# * `env['verikloak.token']` — the raw JWT string
|
|
273
273
|
# * `env['verikloak.user']` — the decoded JWT claims Hash
|
|
@@ -283,7 +283,7 @@ module Verikloak
|
|
|
283
283
|
# @param audience [String] expected `aud` claim
|
|
284
284
|
# @param skip_paths [Array<String>] literal paths or wildcard patterns to bypass auth
|
|
285
285
|
# @param discovery [Discovery, nil] custom discovery instance (for DI/tests)
|
|
286
|
-
# @param jwks_cache [JwksCache, nil] custom
|
|
286
|
+
# @param jwks_cache [JwksCache, nil] custom JWKs cache instance (for DI/tests)
|
|
287
287
|
# @param connection [Faraday::Connection, nil] Optional injected Faraday connection (defaults to Faraday.new)
|
|
288
288
|
# @param leeway [Integer] Clock skew tolerance in seconds for token verification (delegated to TokenDecoder)
|
|
289
289
|
# @param token_verify_options [Hash] Additional JWT verification options passed through
|
|
@@ -332,7 +332,7 @@ module Verikloak
|
|
|
332
332
|
|
|
333
333
|
private
|
|
334
334
|
|
|
335
|
-
# Returns the Faraday connection used for HTTP operations (Discovery/
|
|
335
|
+
# Returns the Faraday connection used for HTTP operations (Discovery/JWKs).
|
|
336
336
|
# Exposed for tests; not part of public API.
|
|
337
337
|
def http_connection
|
|
338
338
|
@connection
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'jwt'
|
|
4
4
|
|
|
5
5
|
module Verikloak
|
|
6
|
-
# Verifies JWT tokens using a
|
|
6
|
+
# Verifies JWT tokens using a JWKs.
|
|
7
7
|
#
|
|
8
8
|
# This class validates a JWT's signature and standard claims (`iss`, `aud`, `exp`, `nbf`, etc.)
|
|
9
9
|
# using the appropriate RSA public key selected by the JWT's `kid` header.
|
|
@@ -24,7 +24,7 @@ module Verikloak
|
|
|
24
24
|
# Default clock skew tolerance in seconds.
|
|
25
25
|
DEFAULT_LEEWAY = 60
|
|
26
26
|
|
|
27
|
-
# Initializes the decoder with a
|
|
27
|
+
# Initializes the decoder with a JWKs and verification criteria.
|
|
28
28
|
#
|
|
29
29
|
# @param jwks [Array<Hash>] List of JWKs from the discovery document.
|
|
30
30
|
# @param issuer [String] Expected `iss` value in the token.
|
|
@@ -106,7 +106,7 @@ module Verikloak
|
|
|
106
106
|
kid = fetch_indifferent(header, 'kid')
|
|
107
107
|
jwk = @jwk_by_kid[kid]
|
|
108
108
|
|
|
109
|
-
raise TokenDecoderError.new("Key with kid=#{kid} not found in
|
|
109
|
+
raise TokenDecoderError.new("Key with kid=#{kid} not found in JWKs", code: 'invalid_token') unless jwk
|
|
110
110
|
|
|
111
111
|
jwk
|
|
112
112
|
end
|
data/lib/verikloak/version.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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -47,20 +47,26 @@ dependencies:
|
|
|
47
47
|
name: jwt
|
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
|
49
49
|
requirements:
|
|
50
|
-
- - "
|
|
50
|
+
- - ">="
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: '2.7'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '4.0'
|
|
53
56
|
type: :runtime
|
|
54
57
|
prerelease: false
|
|
55
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
59
|
requirements:
|
|
57
|
-
- - "
|
|
60
|
+
- - ">="
|
|
58
61
|
- !ruby/object:Gem::Version
|
|
59
62
|
version: '2.7'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '4.0'
|
|
60
66
|
description: |
|
|
61
67
|
Verikloak is a lightweight Ruby gem that provides JWT access token verification middleware
|
|
62
68
|
for Rack-based applications, including Rails API mode. It uses OpenID Connect discovery
|
|
63
|
-
and
|
|
69
|
+
and JWKs to securely validate tokens issued by Keycloak.
|
|
64
70
|
executables: []
|
|
65
71
|
extensions: []
|
|
66
72
|
extra_rdoc_files: []
|
|
@@ -82,7 +88,7 @@ metadata:
|
|
|
82
88
|
source_code_uri: https://github.com/taiyaky/verikloak
|
|
83
89
|
changelog_uri: https://github.com/taiyaky/verikloak/blob/main/CHANGELOG.md
|
|
84
90
|
bug_tracker_uri: https://github.com/taiyaky/verikloak/issues
|
|
85
|
-
documentation_uri: https://rubydoc.info/gems/verikloak/0.1.
|
|
91
|
+
documentation_uri: https://rubydoc.info/gems/verikloak/0.1.3
|
|
86
92
|
rubygems_mfa_required: 'true'
|
|
87
93
|
rdoc_options: []
|
|
88
94
|
require_paths:
|
|
@@ -91,7 +97,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
97
|
requirements:
|
|
92
98
|
- - ">="
|
|
93
99
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '3.
|
|
100
|
+
version: '3.1'
|
|
95
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
102
|
requirements:
|
|
97
103
|
- - ">="
|