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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +2 -1
- data/lib/verikloak/jwks_cache.rb +41 -1
- data/lib/verikloak/token_decoder.rb +16 -9
- data/lib/verikloak/version.rb +1 -1
- metadata +24 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be8192b1562ca02e7da55d8451e5588907707b3677c7343c14fc9448c8dc8cdc
|
|
4
|
+
data.tar.gz: f2eed617dbcae935e8691c72230361da981046414b9c64557d73e5a3a683df42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/verikloak/jwks_cache.rb
CHANGED
|
@@ -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 =
|
|
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>]
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|