rack-jwt-verifier 0.1.0 → 0.3.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 +187 -0
- data/README.md +266 -76
- data/lib/rack-jwt-verifier.rb +5 -0
- data/lib/rack_jwt_verifier/errors.rb +40 -0
- data/lib/rack_jwt_verifier/in_process_cache.rb +61 -17
- data/lib/rack_jwt_verifier/jwt_helper.rb +55 -31
- data/lib/rack_jwt_verifier/key_source.rb +344 -0
- data/lib/rack_jwt_verifier/middleware.rb +221 -30
- data/lib/rack_jwt_verifier/replay_guard.rb +78 -0
- data/lib/rack_jwt_verifier/scopes.rb +43 -0
- data/lib/rack_jwt_verifier/verifier.rb +226 -65
- data/lib/rack_jwt_verifier/version.rb +1 -1
- data/lib/rack_jwt_verifier.rb +10 -11
- metadata +39 -92
- data/.rspec_status +0 -26
- data/Gemfile +0 -14
- data/Gemfile.lock +0 -63
|
@@ -1,99 +1,260 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "jwt"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "in_process_cache"
|
|
6
|
+
require_relative "key_source"
|
|
7
|
+
require_relative "replay_guard"
|
|
6
8
|
|
|
7
9
|
module RackJwtVerifier
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# and
|
|
10
|
+
# Decodes and verifies JWTs against key material from a configured source:
|
|
11
|
+
# a static PEM, a PEM served at a URL, a JWKS endpoint, or a shared HMAC
|
|
12
|
+
# secret. Handles caching, key rotation, claim enforcement and optional jti
|
|
13
|
+
# replay protection so the middleware does not have to.
|
|
11
14
|
class Verifier
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
#
|
|
15
|
+
# Kept under the old constant so existing `rescue Verifier::KeyFetchError`
|
|
16
|
+
# code keeps working.
|
|
17
|
+
KeyFetchError = RackJwtVerifier::KeyFetchError
|
|
18
|
+
|
|
19
|
+
# Cache namespace for a fetched PEM; the full key also carries a digest of the URL.
|
|
20
|
+
PUBLIC_KEY_CACHE_KEY = KeySource::RemotePem::CACHE_KEY_PREFIX
|
|
21
|
+
# Cache namespace for a fetched JWKS; the full key also carries a digest of the URL.
|
|
22
|
+
JWKS_CACHE_KEY = KeySource::RemoteJwks::CACHE_KEY_PREFIX
|
|
23
|
+
# The default TTL for cached key material (5 minutes)
|
|
24
|
+
CACHE_TTL_SECONDS = KeySource::Remote::DEFAULT_CACHE_TTL
|
|
25
|
+
# Default open/read timeout (seconds) for fetching key material
|
|
26
|
+
DEFAULT_HTTP_TIMEOUT = KeySource::Remote::DEFAULT_HTTP_TIMEOUT
|
|
27
|
+
# Largest response body (bytes) accepted as key material
|
|
28
|
+
MAX_KEY_RESPONSE_BYTES = KeySource::Remote::MAX_RESPONSE_BYTES
|
|
29
|
+
# Minimum gap (seconds) between rotation-triggered refetches
|
|
30
|
+
DEFAULT_REFETCH_INTERVAL = KeySource::Remote::DEFAULT_REFETCH_INTERVAL
|
|
31
|
+
|
|
32
|
+
# Exactly one of these tells the Verifier where its key comes from. Having
|
|
33
|
+
# shared_secret in the same exclusive set is what rules out "a secret next
|
|
34
|
+
# to a public key" — the classic RS256/HS256 confusion setup.
|
|
35
|
+
KEY_SOURCE_OPTIONS = %i[public_key public_key_url jwks_url shared_secret].freeze
|
|
36
|
+
|
|
37
|
+
# The HMAC family, only ever enabled by shared_secret.
|
|
38
|
+
HMAC_ALGORITHMS = %w[HS256 HS384 HS512].freeze
|
|
39
|
+
|
|
40
|
+
# RFC 7518 §3.2: an HMAC key must be at least as long as the hash output.
|
|
41
|
+
# Same numbers jwt_auth_client enforces on the signing side.
|
|
42
|
+
MIN_SECRET_BYTES = { "HS256" => 32, "HS384" => 48, "HS512" => 64 }.freeze
|
|
43
|
+
|
|
44
|
+
# Default accepted algorithms per key-source family.
|
|
45
|
+
DEFAULT_ASYMMETRIC_ALGORITHMS = %w[RS256].freeze
|
|
46
|
+
DEFAULT_HMAC_ALGORITHMS = %w[HS256].freeze
|
|
47
|
+
|
|
48
|
+
# ruby-jwt only validates an expected claim value (e.g. `iss: "..."`) when
|
|
49
|
+
# the matching `verify_*` flag is also set. Map each claim to its flag so we
|
|
50
|
+
# can switch the flag on automatically whenever a value is supplied.
|
|
51
|
+
CLAIM_VERIFY_FLAGS = {
|
|
52
|
+
iss: :verify_iss,
|
|
53
|
+
aud: :verify_aud,
|
|
54
|
+
sub: :verify_sub
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Default options for JWT decoding to ensure strict security compliance.
|
|
58
|
+
# ruby-jwt only checks exp/nbf when the claim is present, so `exp` is also
|
|
59
|
+
# listed as required: a token with no expiry would otherwise be valid forever.
|
|
21
60
|
DEFAULT_DECODE_OPTIONS = {
|
|
22
|
-
algorithm: "RS256", # Must match your SSO provider's algorithm
|
|
23
|
-
# THIS MUST BE TRUE: Ensures the 'exp' claim is checked during decoding
|
|
24
61
|
verify_expiration: true,
|
|
25
62
|
verify_not_before: true,
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
# verify_iss: true
|
|
63
|
+
required_claims: %w[exp].freeze,
|
|
64
|
+
leeway: 60 # Allow a 60-second clock skew for "exp" and "nbf" claims
|
|
29
65
|
}.freeze
|
|
30
66
|
|
|
31
67
|
# @param options [Hash] Configuration options.
|
|
32
|
-
# @option options [String] :
|
|
68
|
+
# @option options [String, OpenSSL::PKey] :public_key A static PEM public key or X.509 certificate.
|
|
69
|
+
# @option options [String] :public_key_url The https:// URL serving a PEM public key or certificate.
|
|
70
|
+
# @option options [String] :jwks_url The https:// URL serving a JSON Web Key Set.
|
|
71
|
+
# @option options [String, Hash] :shared_secret An HMAC secret (String) or `{ env: "VAR" }`.
|
|
72
|
+
# Enables the HS* algorithms; mutually exclusive with the public-key sources.
|
|
73
|
+
# @option options [Array<String>] :algorithms Accepted signing algorithms
|
|
74
|
+
# (default: ["RS256"], or ["HS256"] with :shared_secret).
|
|
75
|
+
# @option options [Boolean] :allow_insecure_http Permit a plain http:// URL (development only).
|
|
76
|
+
# @option options [Numeric] :http_timeout Open/read/write timeout in seconds for the key fetch.
|
|
77
|
+
# @option options [Integer] :cache_ttl Seconds to cache fetched key material.
|
|
78
|
+
# @option options [Numeric] :refetch_interval Minimum seconds between rotation-triggered refetches.
|
|
33
79
|
# @option options [Object] :cache_store Optional custom cache object (must respond to #read and #write).
|
|
80
|
+
# @option options [Boolean, Object] :replay_cache `true` to track `jti` in :cache_store (or a fresh
|
|
81
|
+
# InProcessCache), or a cache store to track it in. Tokens are then required to carry a `jti`
|
|
82
|
+
# and a second presentation before `exp` is rejected.
|
|
83
|
+
# @option options [Logger] :logger Where cache-store failures are reported (default: silent).
|
|
34
84
|
# @option options [Hash] :decode_options Custom options for JWT.decode.
|
|
85
|
+
# @raise [ConfigurationError] for an unusable option set.
|
|
35
86
|
def initialize(options = {})
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
# Merge default options over any user-provided options
|
|
43
|
-
@decode_options = DEFAULT_DECODE_OPTIONS.merge(options.fetch(:decode_options, {}))
|
|
87
|
+
source_name = key_source_name(options)
|
|
88
|
+
@key_source = build_key_source(source_name, options)
|
|
89
|
+
@algorithms = build_algorithms(source_name, options)
|
|
90
|
+
@decode_options = build_decode_options(options.fetch(:decode_options, {}), replay: replay_wanted?(options))
|
|
91
|
+
@replay_guard = build_replay_guard(options, @decode_options[:leeway])
|
|
44
92
|
end
|
|
45
93
|
|
|
94
|
+
# The algorithms this verifier accepts, after defaults and policy.
|
|
95
|
+
# @return [Array<String>]
|
|
96
|
+
attr_reader :algorithms
|
|
97
|
+
|
|
46
98
|
# Decodes and verifies the JWT.
|
|
47
99
|
# @param token [String] The JWT string from the Authorization header.
|
|
48
100
|
# @return [Hash] The decoded payload (the user claims).
|
|
49
|
-
# @raise [JWT::DecodeError] If the token is invalid, expired, or signature fails.
|
|
101
|
+
# @raise [JWT::DecodeError] If the token is invalid, expired, replayed, or signature fails.
|
|
102
|
+
# @raise [KeyFetchError] If the key material could not be obtained.
|
|
103
|
+
# @raise [ReplayCacheError] If replay protection is on and its store is unavailable.
|
|
50
104
|
def verify(token)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# The `true` is required to enable verification checks.
|
|
56
|
-
payload, _header = JWT.decode(token, key, true, @decode_options)
|
|
57
|
-
|
|
58
|
-
# For standard usage, we only need the payload hash
|
|
105
|
+
payload = decode_with_rotation(token)
|
|
106
|
+
# Only a token that passed every other check is recorded: an expired or
|
|
107
|
+
# mis-signed token must not be able to "burn" a jti.
|
|
108
|
+
@replay_guard&.check!(payload)
|
|
59
109
|
payload
|
|
60
110
|
end
|
|
61
111
|
|
|
62
112
|
private
|
|
63
113
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
def decode_with_rotation(token)
|
|
115
|
+
decode(token)
|
|
116
|
+
rescue JWT::VerificationError
|
|
117
|
+
# A signature mismatch may mean the provider rotated its key. Refresh
|
|
118
|
+
# once (rate-limited) and retry; if nothing was refreshed, or the retry
|
|
119
|
+
# fails too, the token is simply bad.
|
|
120
|
+
raise unless @key_source.refresh!
|
|
121
|
+
|
|
122
|
+
decode(token)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Performs the cryptographic verification and claim validation. The `true`
|
|
126
|
+
# is required to enable verification checks; only the payload is returned.
|
|
127
|
+
def decode(token)
|
|
128
|
+
payload, _header =
|
|
129
|
+
if @key_source.jwks?
|
|
130
|
+
JWT.decode(token, nil, true, @decode_options.merge(jwks: @key_source.jwks_loader))
|
|
131
|
+
else
|
|
132
|
+
JWT.decode(token, @key_source.verification_key, true, @decode_options)
|
|
133
|
+
end
|
|
134
|
+
payload
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def key_source_name(options)
|
|
138
|
+
given = KEY_SOURCE_OPTIONS.select { |name| options[name] }
|
|
139
|
+
return given.first if given.size == 1
|
|
140
|
+
|
|
141
|
+
raise ConfigurationError,
|
|
142
|
+
"exactly one of #{KEY_SOURCE_OPTIONS.map(&:inspect).join(', ')} must be given" \
|
|
143
|
+
"#{" (got #{given.map(&:inspect).join(' and ')})" unless given.empty?}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_key_source(name, options)
|
|
147
|
+
case name
|
|
148
|
+
when :public_key
|
|
149
|
+
KeySource::Static.new(options[:public_key])
|
|
150
|
+
when :public_key_url
|
|
151
|
+
KeySource::RemotePem.new(url: options[:public_key_url], **remote_options(options))
|
|
152
|
+
when :jwks_url
|
|
153
|
+
KeySource::RemoteJwks.new(url: options[:jwks_url], **remote_options(options))
|
|
154
|
+
when :shared_secret
|
|
155
|
+
KeySource::Secret.new(options[:shared_secret])
|
|
72
156
|
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def remote_options(options)
|
|
160
|
+
{
|
|
161
|
+
# Inject cache store, defaulting to the simple InProcessCache. This
|
|
162
|
+
# allows users to pass in a cache store that responds to #read and #write.
|
|
163
|
+
cache: options.fetch(:cache_store) { InProcessCache.new },
|
|
164
|
+
cache_ttl: options.fetch(:cache_ttl, CACHE_TTL_SECONDS),
|
|
165
|
+
http_timeout: options.fetch(:http_timeout, DEFAULT_HTTP_TIMEOUT),
|
|
166
|
+
refetch_interval: options.fetch(:refetch_interval, DEFAULT_REFETCH_INTERVAL),
|
|
167
|
+
allow_insecure_http: options.fetch(:allow_insecure_http, false),
|
|
168
|
+
logger: options[:logger]
|
|
169
|
+
}
|
|
170
|
+
end
|
|
73
171
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
172
|
+
# The effective algorithm list, from (in order of precedence) a
|
|
173
|
+
# decode_options :algorithm, the top-level :algorithms, a decode_options
|
|
174
|
+
# :algorithms, or the family default — then checked against the policy:
|
|
175
|
+
# never "none", and HS* only with a shared secret, never mixed with
|
|
176
|
+
# asymmetric algorithms. ruby-jwt would happily verify an RS256 token's
|
|
177
|
+
# public key as an HMAC secret otherwise.
|
|
178
|
+
def build_algorithms(source_name, options)
|
|
179
|
+
decode_options = options.fetch(:decode_options, {})
|
|
180
|
+
hmac = source_name == :shared_secret
|
|
181
|
+
|
|
182
|
+
list = decode_options[:algorithm] || options[:algorithms] || decode_options[:algorithms] ||
|
|
183
|
+
(hmac ? DEFAULT_HMAC_ALGORITHMS : DEFAULT_ASYMMETRIC_ALGORITHMS)
|
|
184
|
+
list = Array(list).map { |alg| alg.to_s.strip }.reject(&:empty?)
|
|
185
|
+
raise ConfigurationError, "algorithms must list at least one algorithm" if list.empty?
|
|
186
|
+
|
|
187
|
+
if list.any? { |alg| alg.casecmp?("none") }
|
|
188
|
+
raise ConfigurationError, '"none" is not an acceptable algorithm: it disables signature verification'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
hmac_algs, other_algs = list.partition { |alg| HMAC_ALGORITHMS.include?(alg.upcase) }
|
|
192
|
+
if hmac
|
|
193
|
+
unless other_algs.empty?
|
|
194
|
+
raise ConfigurationError,
|
|
195
|
+
"shared_secret only works with #{HMAC_ALGORITHMS.join('/')}; " \
|
|
196
|
+
"remove #{other_algs.join(', ')} from algorithms or use a public key source"
|
|
197
|
+
end
|
|
198
|
+
check_secret_length!(hmac_algs.map(&:upcase))
|
|
199
|
+
elsif hmac_algs.any?
|
|
200
|
+
raise ConfigurationError,
|
|
201
|
+
"#{hmac_algs.join(', ')} require shared_secret; they cannot be mixed with public-key algorithms " \
|
|
202
|
+
"(a public key would be accepted as the HMAC secret)"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
list.freeze
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def check_secret_length!(hmac_algs)
|
|
209
|
+
needed = hmac_algs.map { |alg| MIN_SECRET_BYTES.fetch(alg) }.max
|
|
210
|
+
actual = @key_source.verification_key.bytesize
|
|
211
|
+
return if actual >= needed
|
|
212
|
+
|
|
213
|
+
raise ConfigurationError,
|
|
214
|
+
"shared_secret must be at least #{needed} bytes for #{hmac_algs.join('/')} (got #{actual}); " \
|
|
215
|
+
"generate one with: openssl rand -hex #{needed}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def replay_wanted?(options)
|
|
219
|
+
store = options[:replay_cache]
|
|
220
|
+
!(store.nil? || store == false)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_replay_guard(options, leeway)
|
|
224
|
+
return nil unless replay_wanted?(options)
|
|
225
|
+
|
|
226
|
+
store = options[:replay_cache]
|
|
227
|
+
store = options.fetch(:cache_store) { InProcessCache.new } if store == true
|
|
228
|
+
unless store.respond_to?(:read) && store.respond_to?(:write)
|
|
229
|
+
raise ConfigurationError, "replay_cache must be true or a cache store responding to #read and #write"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
ReplayGuard.new(store, leeway: leeway)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Merge default options over any user-provided options, then:
|
|
236
|
+
# - install the policy-checked algorithm list and drop any :algorithm so
|
|
237
|
+
# ruby-jwt (which consults :algorithm first) sees exactly that list;
|
|
238
|
+
# - make sure an expected claim value is actually enforced: `iss: "x"` on
|
|
239
|
+
# its own is a no-op in ruby-jwt unless `verify_iss: true` accompanies
|
|
240
|
+
# it. An explicit `verify_*: false` from the user is left untouched;
|
|
241
|
+
# - require a jti when replay protection is on.
|
|
242
|
+
def build_decode_options(user_options, replay:)
|
|
243
|
+
merged = DEFAULT_DECODE_OPTIONS.merge(user_options)
|
|
244
|
+
merged.delete(:algorithm)
|
|
245
|
+
merged[:algorithms] = @algorithms
|
|
246
|
+
|
|
247
|
+
CLAIM_VERIFY_FLAGS.each do |claim, flag|
|
|
248
|
+
merged[flag] = true if merged.key?(claim) && !merged.key?(flag)
|
|
249
|
+
end
|
|
250
|
+
merged[:verify_jti] = true if replay && !merged.key?(:verify_jti)
|
|
77
251
|
|
|
78
|
-
|
|
79
|
-
|
|
252
|
+
leeway = merged[:leeway]
|
|
253
|
+
unless leeway.is_a?(Numeric) && leeway >= 0
|
|
254
|
+
raise ConfigurationError, "decode_options[:leeway] must be a non-negative number of seconds"
|
|
80
255
|
end
|
|
81
256
|
|
|
82
|
-
|
|
83
|
-
public_key_pem = response.body
|
|
84
|
-
|
|
85
|
-
# 4. Cache the new key PEM string
|
|
86
|
-
@cache.write(PUBLIC_KEY_CACHE_KEY, public_key_pem, expires_in: CACHE_TTL_SECONDS)
|
|
87
|
-
|
|
88
|
-
# 5. Return the OpenSSL object for verification
|
|
89
|
-
OpenSSL::PKey::RSA.new(public_key_pem)
|
|
90
|
-
|
|
91
|
-
rescue KeyFetchError
|
|
92
|
-
# Re-raise explicit KeyFetchError for easier debugging/rescue in middleware
|
|
93
|
-
raise
|
|
94
|
-
rescue StandardError => e
|
|
95
|
-
# Catch all other network/parsing/OpenSSL errors
|
|
96
|
-
raise KeyFetchError, "Error processing public key: #{e.message}"
|
|
257
|
+
merged
|
|
97
258
|
end
|
|
98
259
|
end
|
|
99
260
|
end
|
data/lib/rack_jwt_verifier.rb
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Entry point for the gem: `require "rack_jwt_verifier"` loads everything.
|
|
4
|
+
# (`require "rack-jwt-verifier"`, the gem's name, works too.)
|
|
5
5
|
|
|
6
6
|
require_relative "rack_jwt_verifier/version"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
require_relative "rack_jwt_verifier/
|
|
7
|
+
require_relative "rack_jwt_verifier/errors"
|
|
8
|
+
require_relative "rack_jwt_verifier/in_process_cache"
|
|
9
|
+
require_relative "rack_jwt_verifier/key_source"
|
|
10
|
+
require_relative "rack_jwt_verifier/replay_guard"
|
|
11
|
+
require_relative "rack_jwt_verifier/scopes"
|
|
12
12
|
require_relative "rack_jwt_verifier/verifier"
|
|
13
13
|
require_relative "rack_jwt_verifier/middleware"
|
|
14
|
-
require_relative "rack_jwt_verifier/
|
|
15
|
-
|
|
14
|
+
require_relative "rack_jwt_verifier/jwt_helper"
|
|
16
15
|
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# Namespace for the gem: Middleware, Verifier, KeySource, Scopes, ReplayGuard,
|
|
17
|
+
# InProcessCache and the deprecated JwtHelper.
|
|
19
18
|
module RackJwtVerifier
|
|
20
19
|
end
|
metadata
CHANGED
|
@@ -1,146 +1,92 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-jwt-verifier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniele Frisanco
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jwt
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '2.8'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '4'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: '2.8'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
34
|
+
name: logger
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
30
36
|
requirements:
|
|
31
37
|
- - ">="
|
|
32
38
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
39
|
+
version: '1.4'
|
|
34
40
|
type: :runtime
|
|
35
41
|
prerelease: false
|
|
36
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
43
|
requirements:
|
|
38
44
|
- - ">="
|
|
39
45
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: bundler
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - "~>"
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '2.0'
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - "~>"
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '2.0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: rake
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - "~>"
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '13.0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - "~>"
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '13.0'
|
|
46
|
+
version: '1.4'
|
|
69
47
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - "~>"
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '3.0'
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - "~>"
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '3.0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: webmock
|
|
48
|
+
name: rack
|
|
85
49
|
requirement: !ruby/object:Gem::Requirement
|
|
86
50
|
requirements:
|
|
87
|
-
- - "
|
|
51
|
+
- - ">="
|
|
88
52
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
90
|
-
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - "~>"
|
|
53
|
+
version: '2.2'
|
|
54
|
+
- - "<"
|
|
95
55
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
97
|
-
|
|
98
|
-
name: timecop
|
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
|
100
|
-
requirements:
|
|
101
|
-
- - "~>"
|
|
102
|
-
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0.9'
|
|
104
|
-
type: :development
|
|
56
|
+
version: '4'
|
|
57
|
+
type: :runtime
|
|
105
58
|
prerelease: false
|
|
106
59
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
-
requirements:
|
|
108
|
-
- - "~>"
|
|
109
|
-
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0.9'
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: rack-test
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
60
|
requirements:
|
|
115
61
|
- - ">="
|
|
116
62
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
118
|
-
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - ">="
|
|
63
|
+
version: '2.2'
|
|
64
|
+
- - "<"
|
|
123
65
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
125
|
-
description: Verifies JWT
|
|
126
|
-
|
|
127
|
-
|
|
66
|
+
version: '4'
|
|
67
|
+
description: Verifies JWT signatures against a JWKS endpoint, a PEM URL, a static
|
|
68
|
+
key or (opt-in) a shared HMAC secret; enforces exp/nbf/iss/aud, scopes and optional
|
|
69
|
+
jti replay protection; caches key material, handles key rotation, and exposes the
|
|
70
|
+
verified claims to the application through the Rack environment. Pairs with the
|
|
71
|
+
jwt_auth_client gem.
|
|
128
72
|
email:
|
|
129
73
|
- daniele.frisanco@gmail.com
|
|
130
74
|
executables: []
|
|
131
75
|
extensions: []
|
|
132
76
|
extra_rdoc_files: []
|
|
133
77
|
files:
|
|
134
|
-
- ".rspec_status"
|
|
135
78
|
- CHANGELOG.md
|
|
136
|
-
- Gemfile
|
|
137
|
-
- Gemfile.lock
|
|
138
79
|
- LICENSE.md
|
|
139
80
|
- README.md
|
|
81
|
+
- lib/rack-jwt-verifier.rb
|
|
140
82
|
- lib/rack_jwt_verifier.rb
|
|
83
|
+
- lib/rack_jwt_verifier/errors.rb
|
|
141
84
|
- lib/rack_jwt_verifier/in_process_cache.rb
|
|
142
85
|
- lib/rack_jwt_verifier/jwt_helper.rb
|
|
86
|
+
- lib/rack_jwt_verifier/key_source.rb
|
|
143
87
|
- lib/rack_jwt_verifier/middleware.rb
|
|
88
|
+
- lib/rack_jwt_verifier/replay_guard.rb
|
|
89
|
+
- lib/rack_jwt_verifier/scopes.rb
|
|
144
90
|
- lib/rack_jwt_verifier/verifier.rb
|
|
145
91
|
- lib/rack_jwt_verifier/version.rb
|
|
146
92
|
homepage: https://github.com/danielefrisanco/rack_jwt_verifier
|
|
@@ -150,7 +96,8 @@ metadata:
|
|
|
150
96
|
allowed_push_host: https://rubygems.org
|
|
151
97
|
homepage_uri: https://github.com/danielefrisanco/rack_jwt_verifier
|
|
152
98
|
source_code_uri: https://github.com/danielefrisanco/rack_jwt_verifier
|
|
153
|
-
changelog_uri: https://github.com/danielefrisanco/rack_jwt_verifier/CHANGELOG.md
|
|
99
|
+
changelog_uri: https://github.com/danielefrisanco/rack_jwt_verifier/blob/main/CHANGELOG.md
|
|
100
|
+
rubygems_mfa_required: 'true'
|
|
154
101
|
post_install_message:
|
|
155
102
|
rdoc_options: []
|
|
156
103
|
require_paths:
|
|
@@ -159,16 +106,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
159
106
|
requirements:
|
|
160
107
|
- - ">="
|
|
161
108
|
- !ruby/object:Gem::Version
|
|
162
|
-
version:
|
|
109
|
+
version: '3.1'
|
|
163
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
111
|
requirements:
|
|
165
112
|
- - ">="
|
|
166
113
|
- !ruby/object:Gem::Version
|
|
167
114
|
version: '0'
|
|
168
115
|
requirements: []
|
|
169
|
-
rubygems_version: 3.
|
|
116
|
+
rubygems_version: 3.3.26
|
|
170
117
|
signing_key:
|
|
171
118
|
specification_version: 4
|
|
172
|
-
summary:
|
|
173
|
-
|
|
119
|
+
summary: Rack middleware that authenticates requests with JWTs from an external identity
|
|
120
|
+
provider.
|
|
174
121
|
test_files: []
|
data/.rspec_status
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
example_id | status | run_time |
|
|
2
|
-
---------------------------------------------------- | ------ | --------------- |
|
|
3
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:1:1] | passed | 0.06296 seconds |
|
|
4
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:2:1] | passed | 0.05566 seconds |
|
|
5
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:2:2] | passed | 0.03186 seconds |
|
|
6
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:3:1:1] | passed | 0.04063 seconds |
|
|
7
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:3:2:1] | passed | 0.06923 seconds |
|
|
8
|
-
./spec/rack_jwt_verifier/jwt_helper_spec.rb[1:3:3:1] | passed | 0.03564 seconds |
|
|
9
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:1:1] | passed | 0.02249 seconds |
|
|
10
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:2:1:1] | passed | 0.05022 seconds |
|
|
11
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:2:1:2] | passed | 0.02597 seconds |
|
|
12
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:2:2:1] | passed | 0.02197 seconds |
|
|
13
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:2:3:1] | passed | 0.03074 seconds |
|
|
14
|
-
./spec/rack_jwt_verifier/middleware_spec.rb[1:2:4:1] | passed | 0.02496 seconds |
|
|
15
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:1:1] | passed | 0.01668 seconds |
|
|
16
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:1:2] | passed | 0.00633 seconds |
|
|
17
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:1:3] | passed | 0.05474 seconds |
|
|
18
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:1:4] | passed | 0.04057 seconds |
|
|
19
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:2:1] | passed | 0.03279 seconds |
|
|
20
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:2:2] | passed | 0.04898 seconds |
|
|
21
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:2:3:1] | passed | 0.03709 seconds |
|
|
22
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:2:3:2] | passed | 0.01822 seconds |
|
|
23
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:3:1] | passed | 0.01993 seconds |
|
|
24
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:3:2] | passed | 0.01473 seconds |
|
|
25
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:3:3] | passed | 0.03322 seconds |
|
|
26
|
-
./spec/rack_jwt_verifier/verifier_spec.rb[1:3:4] | passed | 0.01368 seconds |
|
data/Gemfile
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
# Specify your gem's dependencies in rack_jwt_verifier.gemspec
|
|
6
|
-
gemspec
|
|
7
|
-
|
|
8
|
-
# Dependencies for development and testing
|
|
9
|
-
group :development, :test do
|
|
10
|
-
gem "rack"
|
|
11
|
-
gem "rspec", "~> 3.12"
|
|
12
|
-
gem "rack-test"
|
|
13
|
-
gem "webmock", "~> 3.14" # Added for mocking network requests in Verifier
|
|
14
|
-
end
|