verikloak 1.0.0 → 1.0.2
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 +19 -0
- data/README.md +18 -5
- data/lib/verikloak/discovery.rb +3 -0
- data/lib/verikloak/jwks_cache.rb +4 -1
- data/lib/verikloak/token_decoder.rb +63 -5
- data/lib/verikloak/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f694670478e8bbeff2369e0898b96a2d675e052bc19c4ac8437bfb3ff209dc52
|
|
4
|
+
data.tar.gz: 8c189a0ae636ee82ab5e0589b047327c6bd5c8360d4a40f3fcbb6ebc1491743b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 456fb93fd6afd7376d4daedc1fe17c708d0b0c7055e5e215874418b84de69fbee048f70fc421560ae3e0296d264f5d4f0be1028382f28798531c292be7bc5ba1
|
|
7
|
+
data.tar.gz: 2f7f2107e34117cbc644b6b1c862da9521b9f1bda47a862ffb6747ad8a1bd851a3217ebc41113552684e1b67887074dfe845a14e6a0ae50cc7aaa6c4318a9665
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.0.2] - 2026-05-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`iat` claim now honors `leeway`** (`TokenDecoder`): On ruby-jwt 3.x the built-in `Claims::IssuedAt` validator ignores the `leeway` option and raises `JWT::InvalidIatError` whenever `iat` is even a fraction of a second in the future. In typical OIDC deployments the IdP (e.g. Keycloak) and the Resource Server run on different hosts/containers, so `iat` is routinely a few hundred milliseconds to a few seconds ahead of the Resource Server's clock and the previously-effective leeway of `0` for `iat` produced spurious 401s. `TokenDecoder` now disables ruby-jwt's built-in `iat` check and performs its own `iat` validation that applies the configured `leeway` consistently with `exp` and `nbf`. Behaviour for tokens with `iat` further in the future than `leeway` is unchanged (still rejected with `invalid_token`). Pass `options: { verify_iat: false }` to skip the check entirely.
|
|
14
|
+
- **`iat` validation now handles symbol-keyed payloads**: `verify_iat_with_leeway!` looks up both `'iat'` and `:iat`, so callers who request symbol-keyed payloads from `JWT.decode` still get the leeway-aware `iat` check applied.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
- **Bumped `rack` to `>= 3.2.6` and `json` to `>= 2.19.5`** in `Gemfile.lock` to clear known advisories surfaced by `bundler-audit` (rack request-smuggling and json format-string injection).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [1.0.1] - 2026-02-15
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **SSRF protection bypass for development mode**: `allow_http: true` now also skips private IP validation in both `JwksCache` (initial `jwks_uri` resolution) and `Discovery` (redirect target resolution). Previously, even with `allow_http: true`, connections to Keycloak on private/loopback IPs (e.g. `127.0.0.1`, `10.x.x.x`, `192.168.x.x`) were blocked by SSRF protection, making local development impossible.
|
|
25
|
+
- **Inconsistent SSRF behaviour**: `Discovery#fetch!` did not validate the initial discovery URL against private IPs (only redirect targets), while `JwksCache` unconditionally blocked private IPs at initialisation. Both now consistently skip private IP checks when `allow_http: true`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
10
29
|
## [1.0.0] - 2026-02-15
|
|
11
30
|
|
|
12
31
|
### Security
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Verikloak is a plug-and-play solution for Ruby (especially Rails API) apps that
|
|
|
18
18
|
- Rails/Rack middleware support
|
|
19
19
|
- Faraday-based customizable HTTP layer
|
|
20
20
|
- HTTPS enforcement for Discovery and JWKs endpoints (with `allow_http:` escape hatch for development)
|
|
21
|
-
- SSRF protection — discovery redirect targets **and** `jwks_uri` values validated against private IP ranges (including IPv4-mapped IPv6 normalisation)
|
|
21
|
+
- SSRF protection — discovery redirect targets **and** `jwks_uri` values validated against private IP ranges (including IPv4-mapped IPv6 normalisation); automatically bypassed with `allow_http: true` for local development
|
|
22
22
|
- HTTPS redirect enforcement — redirect targets are scheme-checked to prevent HTTPS→HTTP downgrade
|
|
23
23
|
- JWT size limit (8 KB) to mitigate denial-of-service via oversized tokens
|
|
24
24
|
- Header injection prevention in `WWW-Authenticate` responses
|
|
@@ -225,7 +225,7 @@ For a full list of error cases and detailed explanations, please see the [ERRORS
|
|
|
225
225
|
| `discovery_redirect_error` | 503 Service Unavailable | Discovery redirect error: missing/invalid Location header, redirect target resolves to a private IP (SSRF protection), redirect uses non-HTTPS scheme, or unsupported scheme |
|
|
226
226
|
| `insecure_discovery_url` | 503 Service Unavailable | Discovery URL uses `http://` and `allow_http: true` is not set |
|
|
227
227
|
| `insecure_jwks_uri` | 503 Service Unavailable | JWKs URI uses `http://` and `allow_http: true` is not set |
|
|
228
|
-
| `jwks_ssrf_blocked` | 503 Service Unavailable | JWKs URI hostname resolves to a private/loopback IP address (SSRF protection)
|
|
228
|
+
| `jwks_ssrf_blocked` | 503 Service Unavailable | JWKs URI hostname resolves to a private/loopback IP address (SSRF protection; bypassed when `allow_http: true`) |
|
|
229
229
|
| `internal_server_error` | 500 Internal Server Error | Unexpected internal error (catch-all) |
|
|
230
230
|
|
|
231
231
|
> **Note:** The `decode_with_public_key` method ensures consistent error codes for all JWT verification failures.
|
|
@@ -250,7 +250,7 @@ For a full list of error cases and detailed explanations, please see the [ERRORS
|
|
|
250
250
|
| `user_env_key` | No | Rack env key for decoded claims. Defaults to `verikloak.user`. |
|
|
251
251
|
| `realm` | No | Value used in the `WWW-Authenticate` header. Defaults to `verikloak`. |
|
|
252
252
|
| `logger` | No | Logger for unexpected internal failures (responds to `error`, optionally `debug`). |
|
|
253
|
-
| `allow_http` | No | When `false` (default), `Discovery` and `JwksCache` reject plain `http://` URLs. Set `true` **only** for local development against a non-TLS Keycloak instance. |
|
|
253
|
+
| `allow_http` | No | When `false` (default), `Discovery` and `JwksCache` reject plain `http://` URLs and block private/loopback IPs (SSRF protection). Set `true` **only** for local development against a non-TLS Keycloak instance — this also bypasses private IP checks so that Keycloak on `localhost` or LAN addresses works out of the box. |
|
|
254
254
|
|
|
255
255
|
#### Option: `skip_paths`
|
|
256
256
|
|
|
@@ -336,8 +336,21 @@ config.middleware.use Verikloak::Middleware,
|
|
|
336
336
|
}
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
- `leeway:` sets the default skew tolerance in seconds.
|
|
340
|
-
|
|
339
|
+
- `leeway:` sets the default skew tolerance in seconds. It is applied to
|
|
340
|
+
`exp`, `nbf`, and `iat`. Verikloak applies leeway to `iat` itself
|
|
341
|
+
because ruby-jwt 3.x's built-in `iat` validator ignores the `leeway`
|
|
342
|
+
option; pass `token_verify_options: { verify_iat: false }` to skip the
|
|
343
|
+
`iat` check entirely.
|
|
344
|
+
- `token_verify_options:` is forwarded to `TokenDecoder` (and ultimately
|
|
345
|
+
to `JWT.decode`), with the following keys handled by Verikloak rather
|
|
346
|
+
than passed through verbatim:
|
|
347
|
+
- `:leeway` — Verikloak forwards it to `JWT.decode` so it applies to
|
|
348
|
+
`exp`/`nbf`, and also uses it for its own `iat` check.
|
|
349
|
+
- `:verify_iat` — ruby-jwt's built-in `iat` validator is always
|
|
350
|
+
disabled (it ignores `:leeway` on ruby-jwt 3.x). Setting
|
|
351
|
+
`verify_iat: false` instead skips Verikloak's own `iat` check;
|
|
352
|
+
setting `verify_iat: true` (the default) keeps it enabled.
|
|
353
|
+
All other keys are forwarded to `JWT.decode` as-is.
|
|
341
354
|
- If both are set, `token_verify_options[:leeway]` takes precedence.
|
|
342
355
|
|
|
343
356
|
## Performance & Caching
|
data/lib/verikloak/discovery.rb
CHANGED
|
@@ -267,10 +267,13 @@ module Verikloak
|
|
|
267
267
|
|
|
268
268
|
# Validates that the redirect target does not resolve to a private/internal IP.
|
|
269
269
|
# IPv4-mapped IPv6 addresses are normalised before comparison.
|
|
270
|
+
# Skipped when `@allow_http` is true (development mode).
|
|
270
271
|
# @api private
|
|
271
272
|
# @param uri [URI] Parsed redirect target
|
|
272
273
|
# @raise [DiscoveryError]
|
|
273
274
|
def validate_redirect_not_private!(uri)
|
|
275
|
+
return if @allow_http
|
|
276
|
+
|
|
274
277
|
host = uri.host
|
|
275
278
|
return unless host
|
|
276
279
|
|
data/lib/verikloak/jwks_cache.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Verikloak
|
|
|
61
61
|
)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
validate_not_private!(clean_jwks_uri)
|
|
64
|
+
validate_not_private!(clean_jwks_uri) unless allow_http
|
|
65
65
|
|
|
66
66
|
@jwks_uri = clean_jwks_uri
|
|
67
67
|
@connection = connection || Verikloak::HTTP.default_connection
|
|
@@ -162,6 +162,9 @@ module Verikloak
|
|
|
162
162
|
|
|
163
163
|
# Validates that the JWKs URI does not resolve to a private/internal IP address (SSRF protection).
|
|
164
164
|
#
|
|
165
|
+
# Skipped when `allow_http: true` is set (development mode), since local Keycloak instances
|
|
166
|
+
# typically run on private/loopback addresses.
|
|
167
|
+
#
|
|
165
168
|
# The discovery redirect flow already validates redirect targets, but the `jwks_uri` value
|
|
166
169
|
# extracted from the discovery JSON document itself was not previously validated, allowing a
|
|
167
170
|
# compromised or malicious discovery endpoint to point JWKs fetching at internal services.
|
|
@@ -8,7 +8,9 @@ module Verikloak
|
|
|
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.
|
|
10
10
|
# Only `RS256`-signed tokens with RSA JWKs are supported.
|
|
11
|
-
# It also supports a configurable clock skew (`leeway`) to account for minor time drift
|
|
11
|
+
# It also supports a configurable clock skew (`leeway`) to account for minor time drift,
|
|
12
|
+
# which is applied uniformly to `exp`, `nbf`, and `iat` (Verikloak applies leeway to
|
|
13
|
+
# `iat` itself because ruby-jwt 3.x's built-in `iat` validator ignores leeway).
|
|
12
14
|
#
|
|
13
15
|
# @example
|
|
14
16
|
# decoder = Verikloak::TokenDecoder.new(
|
|
@@ -41,7 +43,12 @@ module Verikloak
|
|
|
41
43
|
@leeway = leeway
|
|
42
44
|
# Normalize and store verification options
|
|
43
45
|
@options = symbolize_keys(options || {})
|
|
44
|
-
|
|
46
|
+
# Remove keys we manage ourselves so user-supplied values don't
|
|
47
|
+
# re-enable ruby-jwt's built-in (broken w.r.t. leeway) iat validator
|
|
48
|
+
# via the final merge in #jwt_decode_options. The user's intent for
|
|
49
|
+
# :leeway / :verify_iat is still honoured by reading from @options
|
|
50
|
+
# directly elsewhere in this class.
|
|
51
|
+
@options_for_jwt = @options.except(:leeway, :verify_iat).freeze
|
|
45
52
|
|
|
46
53
|
# Build a kid-indexed hash for O(1) JWK lookup
|
|
47
54
|
@jwk_by_kid = {}
|
|
@@ -118,9 +125,49 @@ module Verikloak
|
|
|
118
125
|
# @return [Hash] Verified claims (payload).
|
|
119
126
|
def decode_with_public_key(token, public_key)
|
|
120
127
|
payload, = JWT.decode(token, public_key, true, jwt_decode_options)
|
|
128
|
+
verify_iat_with_leeway!(payload)
|
|
121
129
|
payload
|
|
122
130
|
end
|
|
123
131
|
|
|
132
|
+
# Verifies the `iat` (issued-at) claim with the configured leeway tolerance.
|
|
133
|
+
#
|
|
134
|
+
# ruby-jwt 3.x's built-in `Claims::IssuedAt` validator does not honor the
|
|
135
|
+
# `leeway` option; it raises `JWT::InvalidIatError` whenever `iat` is even
|
|
136
|
+
# a fraction of a second in the future. In OIDC deployments the IdP
|
|
137
|
+
# (e.g. Keycloak) and the Resource Server typically run on different
|
|
138
|
+
# hosts/containers with small clock skew, so `iat` is routinely a few
|
|
139
|
+
# hundred milliseconds to a few seconds in the future relative to the
|
|
140
|
+
# Resource Server's clock. To provide behaviour consistent with `exp`
|
|
141
|
+
# and `nbf` (which honor `leeway`), Verikloak performs its own `iat`
|
|
142
|
+
# check after `JWT.decode` with the configured leeway applied.
|
|
143
|
+
#
|
|
144
|
+
# Users may opt out by passing `options: { verify_iat: false }` to the
|
|
145
|
+
# decoder, in which case this check is skipped entirely.
|
|
146
|
+
#
|
|
147
|
+
# @param payload [Hash] Decoded JWT claims.
|
|
148
|
+
# @return [void]
|
|
149
|
+
# @raise [TokenDecoderError] code: 'invalid_token' when `iat` is not
|
|
150
|
+
# numeric or is further in the future than the allowed leeway.
|
|
151
|
+
def verify_iat_with_leeway!(payload)
|
|
152
|
+
return if @options[:verify_iat] == false
|
|
153
|
+
return unless payload.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
# Support both string- and symbol-keyed payloads. Callers may pass
|
|
156
|
+
# `options: { symbolize_names: true }` which causes JWT.decode to
|
|
157
|
+
# return symbol keys; without indifferent lookup we'd silently
|
|
158
|
+
# skip iat validation while ruby-jwt's check is disabled.
|
|
159
|
+
return unless payload.key?('iat') || payload.key?(:iat)
|
|
160
|
+
|
|
161
|
+
iat = payload.key?('iat') ? payload['iat'] : payload[:iat]
|
|
162
|
+
leeway = @options.key?(:leeway) ? @options[:leeway] : @leeway
|
|
163
|
+
leeway = leeway.to_f
|
|
164
|
+
|
|
165
|
+
return if iat.is_a?(Numeric) && iat.to_f <= Time.now.to_f + leeway
|
|
166
|
+
|
|
167
|
+
raise TokenDecoderError.new('Invalid issued-at (iat) claim',
|
|
168
|
+
code: 'invalid_token')
|
|
169
|
+
end
|
|
170
|
+
|
|
124
171
|
# Returns the verification options passed to JWT.decode.
|
|
125
172
|
#
|
|
126
173
|
# Enforces:
|
|
@@ -129,6 +176,15 @@ module Verikloak
|
|
|
129
176
|
# - Expiration (`exp`) and not-before (`nbf`) checks
|
|
130
177
|
# - Clock skew tolerance via `leeway`
|
|
131
178
|
#
|
|
179
|
+
# NOTE: `verify_iat` is intentionally disabled here. ruby-jwt 3.x's
|
|
180
|
+
# built-in `iat` validator ignores the `leeway` option, which causes
|
|
181
|
+
# spurious `JWT::InvalidIatError` failures whenever the IdP clock is
|
|
182
|
+
# even slightly ahead of the Resource Server's clock. Verikloak
|
|
183
|
+
# re-implements the `iat` check in {#verify_iat_with_leeway!} so the
|
|
184
|
+
# configured `leeway` is honoured consistently with `exp` and `nbf`.
|
|
185
|
+
# Users may still opt out entirely by passing
|
|
186
|
+
# `options: { verify_iat: false }` to the decoder.
|
|
187
|
+
#
|
|
132
188
|
# @return [Hash]
|
|
133
189
|
def jwt_decode_options
|
|
134
190
|
base = {
|
|
@@ -137,15 +193,17 @@ module Verikloak
|
|
|
137
193
|
verify_iss: true,
|
|
138
194
|
aud: @audience,
|
|
139
195
|
verify_aud: true,
|
|
140
|
-
|
|
196
|
+
# See note above: handled by verify_iat_with_leeway! after decode.
|
|
197
|
+
verify_iat: false,
|
|
141
198
|
verify_expiration: true,
|
|
142
199
|
verify_not_before: true
|
|
143
200
|
}
|
|
144
201
|
# options[:leeway] overrides top-level @leeway if provided
|
|
145
202
|
leeway = @options.key?(:leeway) ? @options[:leeway] : @leeway
|
|
146
203
|
merged = base.merge(leeway: leeway)
|
|
147
|
-
# Merge remaining options last (excluding :leeway
|
|
148
|
-
|
|
204
|
+
# Merge remaining options last (excluding :leeway and :verify_iat,
|
|
205
|
+
# which Verikloak manages itself — see initializer comment).
|
|
206
|
+
extra = @options_for_jwt
|
|
149
207
|
merged.merge(extra)
|
|
150
208
|
end
|
|
151
209
|
|
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: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -131,7 +131,7 @@ metadata:
|
|
|
131
131
|
source_code_uri: https://github.com/taiyaky/verikloak
|
|
132
132
|
changelog_uri: https://github.com/taiyaky/verikloak/blob/main/CHANGELOG.md
|
|
133
133
|
bug_tracker_uri: https://github.com/taiyaky/verikloak/issues
|
|
134
|
-
documentation_uri: https://rubydoc.info/gems/verikloak/1.0.
|
|
134
|
+
documentation_uri: https://rubydoc.info/gems/verikloak/1.0.2
|
|
135
135
|
rubygems_mfa_required: 'true'
|
|
136
136
|
rdoc_options: []
|
|
137
137
|
require_paths:
|