verikloak 1.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52b05250c6d7fa0c8a06ac6b3a44f0557f46218a768a662e8a8c43b7419a5849
4
- data.tar.gz: 2d2ce895cf232b4dc06d1427d663b7c9fa140556515bb4fda19693a3cfb11ddf
3
+ metadata.gz: f694670478e8bbeff2369e0898b96a2d675e052bc19c4ac8437bfb3ff209dc52
4
+ data.tar.gz: 8c189a0ae636ee82ab5e0589b047327c6bd5c8360d4a40f3fcbb6ebc1491743b
5
5
  SHA512:
6
- metadata.gz: 98d26aaf0061877278233968ee7fc97e6cdb188c49daa48de7e47c6e2be4c5e84d4dceab064ec4033bac9bede548f1a89cdf8222d4cf8129c2fa7395a8943fb3
7
- data.tar.gz: bf11bba6fbe9a1890e188a4cb72b71dcce1da1b0f8368aee8bffb4d523f2f6c1f4f4c725b0bec8df8b1638010c5bd9499d3157337e9386518ea50cd1329e891b
6
+ metadata.gz: 456fb93fd6afd7376d4daedc1fe17c708d0b0c7055e5e215874418b84de69fbee048f70fc421560ae3e0296d264f5d4f0be1028382f28798531c292be7bc5ba1
7
+ data.tar.gz: 2f7f2107e34117cbc644b6b1c862da9521b9f1bda47a862ffb6747ad8a1bd851a3217ebc41113552684e1b67887074dfe845a14e6a0ae50cc7aaa6c4318a9665
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ 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
+
10
21
  ## [1.0.1] - 2026-02-15
11
22
 
12
23
  ### Fixed
data/README.md CHANGED
@@ -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
- - `token_verify_options:` is passed directly to TokenDecoder (and ultimately to `JWT.decode`).
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
@@ -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
- @options_without_leeway = @options.except(:leeway).freeze
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
- verify_iat: true,
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 which is already applied)
148
- extra = @options_without_leeway
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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Verikloak
4
4
  # Defines the current version of the Verikloak gem.
5
- VERSION = '1.0.1'
5
+ VERSION = '1.0.2'
6
6
  end
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.1
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.1
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: