rack-jwt-verifier 0.1.0 → 0.2.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.
@@ -1,21 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "jwt"
4
- require "net/http"
5
- require "json"
4
+ require_relative "errors"
5
+ require_relative "in_process_cache"
6
+ require_relative "key_source"
6
7
 
7
8
  module RackJwtVerifier
8
- # This class handles the cryptographic heavy lifting: fetching and caching
9
- # public keys from the SSO provider, and performing the actual JWT decoding
10
- # and signature verification.
9
+ # Decodes and verifies JWTs against key material from a configured source:
10
+ # a static PEM, a PEM served at a URL, or a JWKS endpoint. Handles caching,
11
+ # key rotation and claim enforcement so the middleware does not have to.
11
12
  class Verifier
12
- # Error raised if we fail to fetch keys from the remote URL
13
- class KeyFetchError < StandardError; end
14
-
15
- # The cache key used to store the public key PEM string
16
- PUBLIC_KEY_CACHE_KEY = 'rack_jwt_verifier:public_key'.freeze
17
- # The TTL for the cache (5 minutes, must match the default in InProcessCache)
18
- CACHE_TTL_SECONDS = 300
13
+ # Kept under the old constant so existing `rescue Verifier::KeyFetchError`
14
+ # code keeps working.
15
+ KeyFetchError = RackJwtVerifier::KeyFetchError
16
+
17
+ # Cache namespace for a fetched PEM; the full key also carries a digest of the URL.
18
+ PUBLIC_KEY_CACHE_KEY = KeySource::RemotePem::CACHE_KEY_PREFIX
19
+ # Cache namespace for a fetched JWKS; the full key also carries a digest of the URL.
20
+ JWKS_CACHE_KEY = KeySource::RemoteJwks::CACHE_KEY_PREFIX
21
+ # The default TTL for cached key material (5 minutes)
22
+ CACHE_TTL_SECONDS = KeySource::Remote::DEFAULT_CACHE_TTL
23
+ # Default open/read timeout (seconds) for fetching key material
24
+ DEFAULT_HTTP_TIMEOUT = KeySource::Remote::DEFAULT_HTTP_TIMEOUT
25
+ # Largest response body (bytes) accepted as key material
26
+ MAX_KEY_RESPONSE_BYTES = KeySource::Remote::MAX_RESPONSE_BYTES
27
+ # Minimum gap (seconds) between rotation-triggered refetches
28
+ DEFAULT_REFETCH_INTERVAL = KeySource::Remote::DEFAULT_REFETCH_INTERVAL
29
+
30
+ # Exactly one of these tells the Verifier where its key comes from.
31
+ KEY_SOURCE_OPTIONS = %i[public_key public_key_url jwks_url].freeze
32
+
33
+ # ruby-jwt only validates an expected claim value (e.g. `iss: "..."`) when
34
+ # the matching `verify_*` flag is also set. Map each claim to its flag so we
35
+ # can switch the flag on automatically whenever a value is supplied.
36
+ CLAIM_VERIFY_FLAGS = {
37
+ iss: :verify_iss,
38
+ aud: :verify_aud,
39
+ sub: :verify_sub
40
+ }.freeze
19
41
 
20
42
  # Default options for JWT decoding to ensure strict security compliance
21
43
  DEFAULT_DECODE_OPTIONS = {
@@ -23,77 +45,103 @@ module RackJwtVerifier
23
45
  # THIS MUST BE TRUE: Ensures the 'exp' claim is checked during decoding
24
46
  verify_expiration: true,
25
47
  verify_not_before: true,
26
- leeway: 60, # Allow a 60-second clock skew for "exp" and "nbf" claims
27
- # 'iss' validation will be added later when we configure the Verifier.
28
- # verify_iss: true
48
+ leeway: 60 # Allow a 60-second clock skew for "exp" and "nbf" claims
29
49
  }.freeze
30
50
 
31
51
  # @param options [Hash] Configuration options.
32
- # @option options [String] :public_key_url The URL to fetch the public key.
52
+ # @option options [String, OpenSSL::PKey] :public_key A static PEM public key or X.509 certificate.
53
+ # @option options [String] :public_key_url The https:// URL serving a PEM public key or certificate.
54
+ # @option options [String] :jwks_url The https:// URL serving a JSON Web Key Set.
55
+ # @option options [Array<String>] :algorithms Accepted signing algorithms (default: ["RS256"]).
56
+ # @option options [Boolean] :allow_insecure_http Permit a plain http:// URL (development only).
57
+ # @option options [Numeric] :http_timeout Open/read timeout in seconds for the key fetch.
58
+ # @option options [Integer] :cache_ttl Seconds to cache fetched key material.
59
+ # @option options [Numeric] :refetch_interval Minimum seconds between rotation-triggered refetches.
33
60
  # @option options [Object] :cache_store Optional custom cache object (must respond to #read and #write).
61
+ # @option options [Logger] :logger Where cache-store failures are reported (default: silent).
34
62
  # @option options [Hash] :decode_options Custom options for JWT.decode.
35
63
  def initialize(options = {})
36
- @public_key_url = options.fetch(:public_key_url)
37
-
38
- # Inject cache store, defaulting to the simple InProcessCache.
39
- # This allows users to pass in a Redis/Memcached client that responds to #read and #write.
40
- @cache = options.fetch(:cache_store, InProcessCache.new)
41
-
42
- # Merge default options over any user-provided options
43
- @decode_options = DEFAULT_DECODE_OPTIONS.merge(options.fetch(:decode_options, {}))
64
+ @key_source = build_key_source(options)
65
+ @decode_options = build_decode_options(options.fetch(:decode_options, {}), options[:algorithms])
44
66
  end
45
67
 
46
68
  # Decodes and verifies the JWT.
47
69
  # @param token [String] The JWT string from the Authorization header.
48
70
  # @return [Hash] The decoded payload (the user claims).
49
71
  # @raise [JWT::DecodeError] If the token is invalid, expired, or signature fails.
72
+ # @raise [KeyFetchError] If the key material could not be obtained.
50
73
  def verify(token)
51
- # 1. Fetch the key from cache or network
52
- key = fetch_public_key
53
-
54
- # 2. Perform the cryptographic verification and claim validation
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
59
- payload
74
+ decode(token)
75
+ rescue JWT::VerificationError
76
+ # A signature mismatch may mean the provider rotated its key. Refresh
77
+ # once (rate-limited) and retry; if nothing was refreshed, or the retry
78
+ # fails too, the token is simply bad.
79
+ raise unless @key_source.refresh!
80
+
81
+ decode(token)
60
82
  end
61
83
 
62
84
  private
63
85
 
64
- # Handles fetching the public key from the remote URL, using the injected cache.
65
- def fetch_public_key
66
- # 1. Try to read the PEM string from the cache
67
- cached_pem = @cache.read(PUBLIC_KEY_CACHE_KEY)
68
-
69
- if cached_pem
70
- # Found in cache, convert PEM to OpenSSL object and return
71
- return OpenSSL::PKey::RSA.new(cached_pem)
72
- end
86
+ # Performs the cryptographic verification and claim validation. The `true`
87
+ # is required to enable verification checks; only the payload is returned.
88
+ def decode(token)
89
+ payload, _header =
90
+ if @key_source.jwks?
91
+ JWT.decode(token, nil, true, @decode_options.merge(jwks: @key_source.jwks_loader))
92
+ else
93
+ JWT.decode(token, @key_source.verification_key, true, @decode_options)
94
+ end
95
+ payload
96
+ end
73
97
 
74
- # 2. Key is missing or expired, fetch it from the network
75
- uri = URI(@public_key_url)
76
- response = Net::HTTP.get_response(uri)
98
+ def build_key_source(options)
99
+ given = KEY_SOURCE_OPTIONS.select { |name| options[name] }
100
+ unless given.size == 1
101
+ raise ArgumentError,
102
+ "exactly one of #{KEY_SOURCE_OPTIONS.map(&:inspect).join(', ')} must be given" \
103
+ "#{" (got #{given.map(&:inspect).join(' and ')})" unless given.empty?}"
104
+ end
77
105
 
78
- unless response.is_a?(Net::HTTPSuccess)
79
- raise KeyFetchError, "Failed to fetch public key from #{@public_key_url}: #{response.code}"
106
+ case given.first
107
+ when :public_key
108
+ KeySource::Static.new(options[:public_key])
109
+ when :public_key_url
110
+ KeySource::RemotePem.new(url: options[:public_key_url], **remote_options(options))
111
+ when :jwks_url
112
+ KeySource::RemoteJwks.new(url: options[:jwks_url], **remote_options(options))
80
113
  end
114
+ end
81
115
 
82
- # 3. Process response
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}"
116
+ def remote_options(options)
117
+ {
118
+ # Inject cache store, defaulting to the simple InProcessCache. This
119
+ # allows users to pass in a cache store that responds to #read and #write.
120
+ cache: options.fetch(:cache_store) { InProcessCache.new },
121
+ cache_ttl: options.fetch(:cache_ttl, CACHE_TTL_SECONDS),
122
+ http_timeout: options.fetch(:http_timeout, DEFAULT_HTTP_TIMEOUT),
123
+ refetch_interval: options.fetch(:refetch_interval, DEFAULT_REFETCH_INTERVAL),
124
+ allow_insecure_http: options.fetch(:allow_insecure_http, false),
125
+ logger: options[:logger]
126
+ }
127
+ end
128
+
129
+ # Merge default options over any user-provided options, then:
130
+ # - apply a top-level :algorithms list, and drop our :algorithm default
131
+ # whenever a list is in play — ruby-jwt consults :algorithm first, so the
132
+ # default would otherwise silently override the user's list;
133
+ # - make sure an expected claim value is actually enforced: `iss: "x"` on
134
+ # its own is a no-op in ruby-jwt unless `verify_iss: true` accompanies
135
+ # it. An explicit `verify_*: false` from the user is left untouched.
136
+ def build_decode_options(user_options, algorithms)
137
+ merged = DEFAULT_DECODE_OPTIONS.merge(user_options)
138
+ merged[:algorithms] = Array(algorithms) if algorithms
139
+ merged.delete(:algorithm) if merged.key?(:algorithms) && !user_options.key?(:algorithm)
140
+
141
+ CLAIM_VERIFY_FLAGS.each do |claim, flag|
142
+ merged[flag] = true if merged.key?(claim) && !merged.key?(flag)
143
+ end
144
+ merged
97
145
  end
98
146
  end
99
147
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RackJwtVerifier
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,20 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This file serves as the main entry point for the rack_jwt_verifier gem.
4
- # When a user calls `require 'rack_jwt_verifier'`, this file is loaded.
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
- # Require core component files so they are available under the RackJwtVerifier module.
9
- # IMPORTANT: These paths rely on you moving `jwt_helper.rb` into the
10
- # `lib/rack_jwt_verifier/` directory.
11
- require_relative "rack_jwt_verifier/jwt_helper"
7
+ require_relative "rack_jwt_verifier/errors"
8
+ require_relative "rack_jwt_verifier/in_process_cache"
9
+ require_relative "rack_jwt_verifier/key_source"
12
10
  require_relative "rack_jwt_verifier/verifier"
13
11
  require_relative "rack_jwt_verifier/middleware"
14
- require_relative "rack_jwt_verifier/in_process_cache"
15
-
12
+ require_relative "rack_jwt_verifier/jwt_helper"
16
13
 
17
- # The main namespace module for the gem. All classes (JwtHelper, Verifier,
18
- # Middleware) are now accessible via RackJwtVerifier::ClassName.
14
+ # Namespace for the gem: Middleware, Verifier, KeySource, InProcessCache, JwtHelper.
19
15
  module RackJwtVerifier
20
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-jwt-verifier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Frisanco
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-20 00:00:00.000000000 Z
11
+ date: 2026-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -25,121 +25,57 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.8'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rack
28
+ name: logger
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '2.0'
33
+ version: '1.4'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '2.0'
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'
40
+ version: '1.4'
55
41
  - !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'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
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
42
+ name: rack
85
43
  requirement: !ruby/object:Gem::Requirement
86
44
  requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '3.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
45
+ - - ">="
95
46
  - !ruby/object:Gem::Version
96
- version: '3.0'
97
- - !ruby/object:Gem::Dependency
98
- name: timecop
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
47
+ version: '2.2'
48
+ - - "<"
102
49
  - !ruby/object:Gem::Version
103
- version: '0.9'
104
- type: :development
50
+ version: '4'
51
+ type: :runtime
105
52
  prerelease: false
106
53
  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
54
  requirements:
115
55
  - - ">="
116
56
  - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
57
+ version: '2.2'
58
+ - - "<"
123
59
  - !ruby/object:Gem::Version
124
- version: '0'
125
- description: Verifies JWT signature, validates claims (expiration, issuer), and handles
126
- public key retrieval to ensure requests are securely authenticated by an external
127
- SSO provider.
60
+ version: '4'
61
+ description: Verifies JWT signatures against a JWKS endpoint, a PEM URL or a static
62
+ key, enforces exp/nbf/iss/aud claims, caches key material, handles key rotation,
63
+ and exposes the verified claims to the application through the Rack environment.
128
64
  email:
129
65
  - daniele.frisanco@gmail.com
130
66
  executables: []
131
67
  extensions: []
132
68
  extra_rdoc_files: []
133
69
  files:
134
- - ".rspec_status"
135
70
  - CHANGELOG.md
136
- - Gemfile
137
- - Gemfile.lock
138
71
  - LICENSE.md
139
72
  - README.md
73
+ - lib/rack-jwt-verifier.rb
140
74
  - lib/rack_jwt_verifier.rb
75
+ - lib/rack_jwt_verifier/errors.rb
141
76
  - lib/rack_jwt_verifier/in_process_cache.rb
142
77
  - lib/rack_jwt_verifier/jwt_helper.rb
78
+ - lib/rack_jwt_verifier/key_source.rb
143
79
  - lib/rack_jwt_verifier/middleware.rb
144
80
  - lib/rack_jwt_verifier/verifier.rb
145
81
  - lib/rack_jwt_verifier/version.rb
@@ -150,7 +86,8 @@ metadata:
150
86
  allowed_push_host: https://rubygems.org
151
87
  homepage_uri: https://github.com/danielefrisanco/rack_jwt_verifier
152
88
  source_code_uri: https://github.com/danielefrisanco/rack_jwt_verifier
153
- changelog_uri: https://github.com/danielefrisanco/rack_jwt_verifier/CHANGELOG.md
89
+ changelog_uri: https://github.com/danielefrisanco/rack_jwt_verifier/blob/main/CHANGELOG.md
90
+ rubygems_mfa_required: 'true'
154
91
  post_install_message:
155
92
  rdoc_options: []
156
93
  require_paths:
@@ -159,16 +96,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
159
96
  requirements:
160
97
  - - ">="
161
98
  - !ruby/object:Gem::Version
162
- version: 2.6.6
99
+ version: '3.0'
163
100
  required_rubygems_version: !ruby/object:Gem::Requirement
164
101
  requirements:
165
102
  - - ">="
166
103
  - !ruby/object:Gem::Version
167
104
  version: '0'
168
105
  requirements: []
169
- rubygems_version: 3.2.3
106
+ rubygems_version: 3.3.26
170
107
  signing_key:
171
108
  specification_version: 4
172
- summary: A Rack middleware for authenticating requests using JWTs (JSON Web Tokens)
173
- and injecting user data into the Rack environment.
109
+ summary: Rack middleware that authenticates requests with JWTs from an external identity
110
+ provider.
174
111
  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
data/Gemfile.lock DELETED
@@ -1,63 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- rack-jwt-verifier (0.1.0)
5
- jwt (~> 2.8)
6
- rack (>= 2.0)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- addressable (2.8.7)
12
- public_suffix (>= 2.0.2, < 7.0)
13
- base64 (0.2.0)
14
- bigdecimal (3.1.8)
15
- crack (1.0.0)
16
- bigdecimal
17
- rexml
18
- diff-lcs (1.6.2)
19
- hashdiff (1.1.0)
20
- jwt (2.8.2)
21
- base64
22
- public_suffix (5.1.1)
23
- rack (3.2.3)
24
- rack-test (2.2.0)
25
- rack (>= 1.3)
26
- rake (13.2.1)
27
- rexml (3.3.2)
28
- strscan
29
- rspec (3.13.1)
30
- rspec-core (~> 3.13.0)
31
- rspec-expectations (~> 3.13.0)
32
- rspec-mocks (~> 3.13.0)
33
- rspec-core (3.13.5)
34
- rspec-support (~> 3.13.0)
35
- rspec-expectations (3.13.5)
36
- diff-lcs (>= 1.2.0, < 2.0)
37
- rspec-support (~> 3.13.0)
38
- rspec-mocks (3.13.6)
39
- diff-lcs (>= 1.2.0, < 2.0)
40
- rspec-support (~> 3.13.0)
41
- rspec-support (3.13.6)
42
- strscan (3.1.0)
43
- timecop (0.9.10)
44
- webmock (3.23.1)
45
- addressable (>= 2.8.0)
46
- crack (>= 0.3.2)
47
- hashdiff (>= 0.4.0, < 2.0.0)
48
-
49
- PLATFORMS
50
- x86_64-linux
51
-
52
- DEPENDENCIES
53
- bundler (~> 2.0)
54
- rack
55
- rack-jwt-verifier!
56
- rack-test
57
- rake (~> 13.0)
58
- rspec (~> 3.12)
59
- timecop (~> 0.9)
60
- webmock (~> 3.14)
61
-
62
- BUNDLED WITH
63
- 2.4.22