verikloak 0.4.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2acaa72f8a2eee680d3c439b99488a48e7f512de4fb1ab2092ff781f13799c89
4
- data.tar.gz: 5918e081c8be11ef410170f38cd4f520c2cfe55a5b34ac9e5363eddedc297a75
3
+ metadata.gz: be8192b1562ca02e7da55d8451e5588907707b3677c7343c14fc9448c8dc8cdc
4
+ data.tar.gz: f2eed617dbcae935e8691c72230361da981046414b9c64557d73e5a3a683df42
5
5
  SHA512:
6
- metadata.gz: 8ee86f3b6586beac55b0a4e54f9815f58c99edf57bb2661851ae39e4f9f0bab80126643b903802ec90c266ca9fd16bac92550fd18d0c21be52487bfbb80c756e
7
- data.tar.gz: 83f47c238874faee6b3391a833e7e3966b04637b3859b64e01943243d5aa1c64bf22986586a00c85ac651928cec476b7a8e05b030fd23172abf5ce60a3ff0e65
6
+ metadata.gz: a5b3aef46de49a76e52a19edf022a079a343df6ec583da65f193708a0dbd950874d2a2f0e1538f3748f7ce77c78da1ff7386ee177628f8054937f2d08ddb4e22
7
+ data.tar.gz: 5160853c31143b7581b512022047ab4990d39bda1644d7d57622ca22cfed15506669a10a77c809af0e220cbddd4e7b347ff58bd96037d5d6ec6605894a3910b5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.0.0] - 2026-02-15
11
+
12
+ ### Security
13
+ - **JWKs URI SSRF protection**: `JwksCache` now validates `jwks_uri` from discovery documents against private IP ranges (RFC 1918, loopback, link-local), preventing a malicious discovery endpoint from directing JWKs fetches to internal services
14
+
15
+ ### Fixed
16
+ - **`TokenDecoder` error classification**: `invalid_signature` and `unsupported_algorithm` error codes were never returned due to dead `case/when` branches — replaced with `if/elsif` chain so `JWT::VerificationError` and `JWT::IncorrectAlgorithm` are classified correctly
17
+
18
+ ### Added
19
+ - **`rack` runtime dependency**: Added `rack >= 2.2, < 4.0` to gemspec (was previously required but undeclared)
20
+
21
+ ### Changed
22
+ - **`json` dependency relaxed**: `~> 2.18` → `~> 2.6` to broaden compatibility with older Ruby versions and bundled json gems
23
+ - **v1.0.0 stable release**: Public API is now considered stable under Semantic Versioning
24
+
25
+ ---
26
+
10
27
  ## [0.4.1] - 2026-02-15
11
28
 
12
29
  ### Added
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 — redirect targets 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)
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,6 +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
229
  | `internal_server_error` | 500 Internal Server Error | Unexpected internal error (catch-all) |
229
230
 
230
231
  > **Note:** The `decode_with_public_key` method ensures consistent error codes for all JWT verification failures.
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'faraday'
4
+ require 'ipaddr'
4
5
  require 'json'
6
+ require 'resolv'
7
+ require 'uri'
5
8
 
6
9
  require 'verikloak/http'
7
10
 
@@ -39,7 +42,7 @@ module Verikloak
39
42
  # @param jwks_uri [String] HTTPS URL of the JWKs endpoint
40
43
  # @param connection [Faraday::Connection, nil] Optional Faraday connection for HTTP requests
41
44
  # @param allow_http [Boolean] When false (default), raises on plain HTTP URIs. Set true for local development only.
42
- # @raise [JwksCacheError] if the URI is not an HTTP(S) URL
45
+ # @raise [JwksCacheError] if the URI is not an HTTP(S) URL or resolves to a private/internal address
43
46
  def initialize(jwks_uri:, connection: nil, allow_http: false)
44
47
  unless jwks_uri.is_a?(String)
45
48
  raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed')
@@ -58,6 +61,8 @@ module Verikloak
58
61
  )
59
62
  end
60
63
 
64
+ validate_not_private!(clean_jwks_uri)
65
+
61
66
  @jwks_uri = clean_jwks_uri
62
67
  @connection = connection || Verikloak::HTTP.default_connection
63
68
  @cached_keys = nil
@@ -155,6 +160,41 @@ module Verikloak
155
160
  (Time.now - @fetched_at) < @max_age
156
161
  end
157
162
 
163
+ # Validates that the JWKs URI does not resolve to a private/internal IP address (SSRF protection).
164
+ #
165
+ # The discovery redirect flow already validates redirect targets, but the `jwks_uri` value
166
+ # extracted from the discovery JSON document itself was not previously validated, allowing a
167
+ # compromised or malicious discovery endpoint to point JWKs fetching at internal services.
168
+ #
169
+ # IPv4-mapped IPv6 addresses (e.g. `::ffff:127.0.0.1`) are normalised to their native IPv4
170
+ # form before comparison, consistent with {Verikloak::Discovery}.
171
+ #
172
+ # @api private
173
+ # @param url [String] The JWKs URI to validate
174
+ # @raise [JwksCacheError] when the URI resolves to a private/internal address
175
+ def validate_not_private!(url)
176
+ uri = URI.parse(url)
177
+ host = uri.host
178
+ return unless host
179
+
180
+ Resolv.getaddresses(host).each do |addr|
181
+ ip = IPAddr.new(addr)
182
+ ip = ip.native if ip.ipv4_mapped?
183
+ next unless Verikloak::PRIVATE_IP_RANGES.any? { |range| range.include?(ip) }
184
+
185
+ raise JwksCacheError.new(
186
+ "JWKs URI resolves to a private/internal address (#{host})",
187
+ code: 'jwks_ssrf_blocked'
188
+ )
189
+ end
190
+ rescue URI::InvalidURIError => e
191
+ raise JwksCacheError.new("Invalid JWKs URI: #{e.message}", code: 'jwks_fetch_failed')
192
+ rescue IPAddr::InvalidAddressError
193
+ # If the address cannot be parsed, allow the request to proceed
194
+ # (Faraday will handle the actual connection error)
195
+ nil
196
+ end
197
+
158
198
  # @api private
159
199
  # Parses a `Cache-Control` header and extracts `max-age` in seconds.
160
200
  #
@@ -199,20 +199,27 @@ module Verikloak
199
199
  # Pass through our own structured errors without rewrapping
200
200
  raise e
201
201
  rescue *jwt_errors => e
202
- code = case e
203
- when JWT::ExpiredSignature then 'expired_token'
204
- when JWT::ImmatureSignature then 'not_yet_valid'
205
- when JWT::InvalidIssuerError then 'invalid_issuer'
206
- when JWT::InvalidAudError then 'invalid_audience'
207
- when defined?(JWT::VerificationError) && e.is_a?(JWT::VerificationError) then 'invalid_signature'
208
- when defined?(JWT::IncorrectAlgorithm) && e.is_a?(JWT::IncorrectAlgorithm) then 'unsupported_algorithm'
209
- else 'invalid_token'
210
- end
202
+ code = classify_jwt_error(e)
211
203
  raise TokenDecoderError.new(jwt_error_message(e), code: code)
212
204
  rescue StandardError => e
213
205
  raise TokenDecoderError.new("Unexpected token verification error: #{e.message}", code: 'invalid_token')
214
206
  end
215
207
 
208
+ # Map a JWT exception to a machine-friendly error code.
209
+ #
210
+ # @param error [Exception]
211
+ # @return [String]
212
+ def classify_jwt_error(error)
213
+ return 'expired_token' if error.is_a?(JWT::ExpiredSignature)
214
+ return 'not_yet_valid' if error.is_a?(JWT::ImmatureSignature)
215
+ return 'invalid_issuer' if error.is_a?(JWT::InvalidIssuerError)
216
+ return 'invalid_audience' if error.is_a?(JWT::InvalidAudError)
217
+ return 'unsupported_algorithm' if defined?(JWT::IncorrectAlgorithm) && error.is_a?(JWT::IncorrectAlgorithm)
218
+ return 'invalid_signature' if defined?(JWT::VerificationError) && error.is_a?(JWT::VerificationError)
219
+
220
+ 'invalid_token'
221
+ end
222
+
216
223
  # JWT-related exceptions to catch and rewrap.
217
224
  #
218
225
  # @return [Array<Class>]
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Verikloak
4
4
  # Defines the current version of the Verikloak gem.
5
- VERSION = '0.4.1'
5
+ VERSION = '1.0.0'
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: 0.4.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -55,14 +55,14 @@ dependencies:
55
55
  requirements:
56
56
  - - "~>"
57
57
  - !ruby/object:Gem::Version
58
- version: '2.18'
58
+ version: '2.6'
59
59
  type: :runtime
60
60
  prerelease: false
61
61
  version_requirements: !ruby/object:Gem::Requirement
62
62
  requirements:
63
63
  - - "~>"
64
64
  - !ruby/object:Gem::Version
65
- version: '2.18'
65
+ version: '2.6'
66
66
  - !ruby/object:Gem::Dependency
67
67
  name: jwt
68
68
  requirement: !ruby/object:Gem::Requirement
@@ -83,6 +83,26 @@ dependencies:
83
83
  - - "<"
84
84
  - !ruby/object:Gem::Version
85
85
  version: '4.0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rack
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '2.2'
93
+ - - "<"
94
+ - !ruby/object:Gem::Version
95
+ version: '4.0'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '2.2'
103
+ - - "<"
104
+ - !ruby/object:Gem::Version
105
+ version: '4.0'
86
106
  description: |
87
107
  Verikloak is a lightweight Ruby gem that provides JWT access token verification middleware
88
108
  for Rack-based applications, including Rails API mode. It uses OpenID Connect discovery
@@ -111,7 +131,7 @@ metadata:
111
131
  source_code_uri: https://github.com/taiyaky/verikloak
112
132
  changelog_uri: https://github.com/taiyaky/verikloak/blob/main/CHANGELOG.md
113
133
  bug_tracker_uri: https://github.com/taiyaky/verikloak/issues
114
- documentation_uri: https://rubydoc.info/gems/verikloak/0.4.1
134
+ documentation_uri: https://rubydoc.info/gems/verikloak/1.0.0
115
135
  rubygems_mfa_required: 'true'
116
136
  rdoc_options: []
117
137
  require_paths: