jwt 2.4.1 → 2.5.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: 6e7f3474ee58d51ca5646f48ca28bf669b40a4b7676cbe7211597ca6ae69f672
4
- data.tar.gz: 570e6930c9094afea40ea8e8a6a7c9b3293890b121893f5148914b0a8e7d11f8
3
+ metadata.gz: a3098671a837e7b291103cde1921277c61ecaa0f0797b955e6adc65328498f0d
4
+ data.tar.gz: 3253833ac6d7743e40a5d5157b161cd0daecc9b77f61dfa7687d6b3da1be56ca
5
5
  SHA512:
6
- metadata.gz: 3249529ec6bacc8e655e2830949af61c10e235a569f9dc67d3880335d5939b8afc56c180145d3e02dd09744288d50c31547338e105cf55ae4e0fbe237eb2a0e8
7
- data.tar.gz: dd415314a7bd048d8b2b5b630d5b7011128932bf207dc785ac6154748aff68836a1c39e766dc176e225c643fc406fe9fdc5c510b36dc939e36722e327d8fe92f
6
+ metadata.gz: 306c946b1199301a3f1000c8ffba4a77d07fd05dd83f769da86fd29f254827b5af8488a4b6a54b11f1f7f3a028cb88caafb7ed67528e7004c0337f6506e595ea
7
+ data.tar.gz: 57d1eba7a06bc9d9f9fcb76b42aa3808415af5020c53969b4cada890b1646e7d348a96ce18010ab0a978e42825febbeb7b3f205b72e8ce60ef90132cf5887599
@@ -13,7 +13,7 @@ jobs:
13
13
  timeout-minutes: 30
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v2
16
+ - uses: actions/checkout@v3
17
17
  - name: Set up Ruby
18
18
  uses: ruby/setup-ruby@v1
19
19
  with:
@@ -22,34 +22,35 @@ jobs:
22
22
  - name: Run RuboCop
23
23
  run: bundle exec rubocop
24
24
  test:
25
+ name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }}
26
+ runs-on: ${{ matrix.os }}
25
27
  strategy:
26
28
  fail-fast: false
27
29
  matrix:
30
+ os:
31
+ - ubuntu-20.04
28
32
  ruby:
29
- - 2.5
30
- - 2.6
31
- - 2.7
33
+ - "2.5"
34
+ - "2.6"
35
+ - "2.7"
32
36
  - "3.0"
33
- - 3.1
37
+ - "3.1"
34
38
  gemfile:
35
39
  - gemfiles/standalone.gemfile
36
40
  - gemfiles/openssl.gemfile
37
41
  - gemfiles/rbnacl.gemfile
38
42
  experimental: [false]
39
43
  include:
40
- - ruby: 2.7
41
- gemfile: 'gemfiles/rbnacl.gemfile'
42
- - ruby: "ruby-head"
43
- experimental: true
44
- - ruby: "truffleruby-head"
45
- experimental: true
46
- runs-on: ubuntu-20.04
44
+ - { os: ubuntu-20.04, ruby: "2.7", gemfile: 'gemfiles/rbnacl.gemfile', experimental: false }
45
+ - { os: ubuntu-22.04, ruby: "3.1", experimental: false }
46
+ - { os: ubuntu-20.04, ruby: "truffleruby-head", experimental: true }
47
+ - { os: ubuntu-22.04, ruby: "head", experimental: true }
47
48
  continue-on-error: ${{ matrix.experimental }}
48
49
  env:
49
50
  BUNDLE_GEMFILE: ${{ matrix.gemfile }}
50
51
 
51
52
  steps:
52
- - uses: actions/checkout@v2
53
+ - uses: actions/checkout@v3
53
54
 
54
55
  - name: Install libsodium
55
56
  run: |
data/.rubocop.yml CHANGED
@@ -29,7 +29,7 @@ Metrics/AbcSize:
29
29
  Max: 25
30
30
 
31
31
  Metrics/ClassLength:
32
- Max: 105
32
+ Max: 112
33
33
 
34
34
  Metrics/ModuleLength:
35
35
  Max: 100
data/CHANGELOG.md CHANGED
@@ -1,11 +1,30 @@
1
1
  # Changelog
2
- ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
2
+
3
+
4
+ ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (NEXT)
5
+
6
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...master)
7
+
8
+ **Features:**
9
+
10
+ - Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)).
11
+ - Your contribution here
3
12
 
4
13
  **Fixes and enhancements:**
5
- - Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)).
14
+ - Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)).
15
+ - Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)).
16
+ - New parameter name for cases when kid is not found using JWK key loader proc [#501](https://github.com/jwt/ruby-jwt/pull/501) ([@anakinj](https://github.com/anakinj)).
17
+ - Fix NoMethodError when a 2 segment token is missing 'alg' header [#502](https://github.com/jwt/ruby-jwt/pull/502) ([@cmrd-senya](https://github.com/cmrd-senya)).
18
+ - Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)).
19
+ - Your contribution here
20
+
21
+ ## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07)
6
22
 
7
23
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.0...v2.4.1)
8
24
 
25
+ **Fixes and enhancements:**
26
+ - Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)).
27
+
9
28
  ## [v2.4.0](https://github.com/jwt/ruby-jwt/tree/v2.4.0) (2022-06-06)
10
29
 
11
30
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.3.0...v2.4.0)
data/README.md CHANGED
@@ -12,7 +12,7 @@ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools
12
12
  If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
13
13
 
14
14
  ## Announcements
15
- * Ruby 2.4 support is going to be dropped in version 2.4.0
15
+ * Ruby 2.4 support was dropped in version 2.4.0
16
16
  * Ruby 1.9.3 support was dropped at December 31st, 2016.
17
17
  * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
18
18
 
@@ -135,17 +135,14 @@ puts decoded_token
135
135
  * ES256K - ECDSA using P-256K and SHA-256
136
136
 
137
137
  ```ruby
138
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
139
- ecdsa_key.generate_key
140
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
141
- ecdsa_public.private_key = nil
138
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
142
139
 
143
140
  token = JWT.encode payload, ecdsa_key, 'ES256'
144
141
 
145
142
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
146
143
  puts token
147
144
 
148
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
145
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
149
146
 
150
147
  # Array
151
148
  # [
@@ -186,7 +183,7 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
186
183
 
187
184
  ### **RSASSA-PSS**
188
185
 
189
- In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`.
186
+ In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
190
187
 
191
188
  ```ruby
192
189
  gem 'openssl', '~> 2.1'
@@ -546,30 +543,41 @@ end
546
543
 
547
544
  ### JSON Web Key (JWK)
548
545
 
549
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys.
546
+ JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved.
550
547
 
551
- ```ruby
552
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
553
- payload, headers = { data: 'data' }, { kid: jwk.kid }
554
-
555
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
548
+ If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
556
549
 
557
- # The jwk loader would fetch the set of JWKs from a trusted source
558
- jwk_loader = ->(options) do
559
- @cached_keys = nil if options[:invalidate] # need to reload the keys
560
- @cached_keys ||= { keys: [jwk.export] }
561
- end
550
+ ```ruby
551
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid')
552
+ payload = { data: 'data' }
553
+ headers = { kid: jwk.kid }
554
+
555
+ token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
556
+
557
+ # The jwk loader would fetch the set of JWKs from a trusted source,
558
+ # to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation.
559
+ # This example only allows cache invalidations every 5 minutes.
560
+ jwk_loader = ->(options) do
561
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
562
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
563
+ @cached_keys = nil
564
+ end
565
+ @cached_keys ||= begin
566
+ @cache_last_update = Time.now.to_i
567
+ { keys: [jwk.export] }
568
+ end
569
+ end
562
570
 
563
- begin
564
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
565
- rescue JWT::JWKError
566
- # Handle problems with the provided JWKs
567
- rescue JWT::DecodeError
568
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
569
- end
571
+ begin
572
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
573
+ rescue JWT::JWKError
574
+ # Handle problems with the provided JWKs
575
+ rescue JWT::DecodeError
576
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
577
+ end
570
578
  ```
571
579
 
572
- or by passing JWK as a simple Hash
580
+ or by passing the JWKs as a simple Hash
573
581
 
574
582
  ```
575
583
  jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
@@ -587,8 +595,39 @@ jwk_hash = jwk.export
587
595
  jwk_hash_with_private_key = jwk.export(include_private: true)
588
596
  ```
589
597
 
590
- ## How to contribute
598
+ ### Key ID (kid) and JWKs
599
+
600
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization.
601
+
602
+ ```ruby
603
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
604
+ # OR
605
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
606
+ # OR
607
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint)
608
+
609
+ jwk_hash = jwk.export
610
+
611
+ thumbprint_as_the_kid = jwk_hash[:kid]
612
+
613
+ ```
614
+
615
+ # Development and Tests
616
+
617
+ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
618
+
619
+ ```bash
620
+ rake release
621
+ ```
591
622
 
623
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
624
+
625
+ ```bash
626
+ bundle install
627
+ bundle exec appraisal rake test
628
+ ```
629
+
630
+ ## How to contribute
592
631
  See [CONTRIBUTING](CONTRIBUTING.md).
593
632
 
594
633
  ## Contributors
@@ -27,6 +27,8 @@ module JWT
27
27
  raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
28
28
 
29
29
  public_key.verify(signature, signing_input)
30
+ rescue RbNaCl::CryptoError
31
+ false
30
32
  end
31
33
  end
32
34
  end
data/lib/jwt/base64.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module JWT
6
+ # Base64 helpers
7
+ class Base64
8
+ class << self
9
+ def url_encode(str)
10
+ ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
11
+ end
12
+
13
+ def url_decode(str)
14
+ str += '=' * (4 - str.length.modulo(4))
15
+ ::Base64.decode64(str.tr('-_', '+/'))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decode_configuration'
4
+ require_relative 'jwk_configuration'
5
+
6
+ module JWT
7
+ module Configuration
8
+ class Container
9
+ attr_accessor :decode, :jwk
10
+
11
+ def initialize
12
+ reset!
13
+ end
14
+
15
+ def reset!
16
+ @decode = DecodeConfiguration.new
17
+ @jwk = JwkConfiguration.new
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Configuration
5
+ class DecodeConfiguration
6
+ attr_accessor :verify_expiration,
7
+ :verify_not_before,
8
+ :verify_iss,
9
+ :verify_iat,
10
+ :verify_jti,
11
+ :verify_aud,
12
+ :verify_sub,
13
+ :leeway,
14
+ :algorithms,
15
+ :required_claims
16
+
17
+ def initialize
18
+ @verify_expiration = true
19
+ @verify_not_before = true
20
+ @verify_iss = false
21
+ @verify_iat = false
22
+ @verify_jti = false
23
+ @verify_aud = false
24
+ @verify_sub = false
25
+ @leeway = 0
26
+ @algorithms = ['HS256']
27
+ @required_claims = []
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ verify_expiration: verify_expiration,
33
+ verify_not_before: verify_not_before,
34
+ verify_iss: verify_iss,
35
+ verify_iat: verify_iat,
36
+ verify_jti: verify_jti,
37
+ verify_aud: verify_aud,
38
+ verify_sub: verify_sub,
39
+ leeway: leeway,
40
+ algorithms: algorithms,
41
+ required_claims: required_claims
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../jwk/kid_as_key_digest'
4
+ require_relative '../jwk/thumbprint'
5
+
6
+ module JWT
7
+ module Configuration
8
+ class JwkConfiguration
9
+ def initialize
10
+ self.kid_generator_type = :key_digest
11
+ end
12
+
13
+ def kid_generator_type=(value)
14
+ self.kid_generator = case value
15
+ when :key_digest
16
+ JWT::JWK::KidAsKeyDigest
17
+ when :rfc7638_thumbprint
18
+ JWT::JWK::Thumbprint
19
+ else
20
+ raise ArgumentError, "#{value} is not a valid kid generator type."
21
+ end
22
+ end
23
+
24
+ attr_accessor :kid_generator
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration/container'
4
+
5
+ module JWT
6
+ module Configuration
7
+ def configure
8
+ yield(configuration)
9
+ end
10
+
11
+ def configuration
12
+ @configuration ||= ::JWT::Configuration::Container.new
13
+ end
14
+ end
15
+ end
data/lib/jwt/decode.rb CHANGED
@@ -113,13 +113,11 @@ module JWT
113
113
  end
114
114
 
115
115
  def none_algorithm?
116
- algorithm.casecmp('none').zero?
116
+ algorithm == 'none'
117
117
  end
118
118
 
119
119
  def decode_crypto
120
- @signature = Base64.urlsafe_decode64(@segments[2] || '')
121
- rescue ArgumentError
122
- raise(JWT::DecodeError, 'Invalid segment encoding')
120
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
123
121
  end
124
122
 
125
123
  def algorithm
@@ -139,8 +137,8 @@ module JWT
139
137
  end
140
138
 
141
139
  def parse_and_decode(segment)
142
- JWT::JSON.parse(Base64.urlsafe_decode64(segment))
143
- rescue ::JSON::ParserError, ArgumentError
140
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
141
+ rescue ::JSON::ParserError
144
142
  raise JWT::DecodeError, 'Invalid segment encoding'
145
143
  end
146
144
  end
data/lib/jwt/encode.rb CHANGED
@@ -55,11 +55,11 @@ module JWT
55
55
  def encode_signature
56
56
  return '' if @algorithm == ALG_NONE
57
57
 
58
- Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
58
+ ::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
59
59
  end
60
60
 
61
61
  def encode(data)
62
- Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false)
62
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
63
  end
64
64
 
65
65
  def combine(*parts)
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,39 +4,53 @@ require 'forwardable'
4
4
 
5
5
  module JWT
6
6
  module JWK
7
- class EC < KeyBase
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
8
  extend Forwardable
9
- def_delegators :@keypair, :public_key
9
+ def_delegators :keypair, :public_key
10
10
 
11
11
  KTY = 'EC'
12
12
  KTYS = [KTY, OpenSSL::PKey::EC].freeze
13
13
  BINARY = 2
14
14
 
15
- def initialize(keypair, kid = nil)
15
+ attr_reader :keypair
16
+
17
+ def initialize(keypair, options = {})
16
18
  raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
17
19
 
18
- kid ||= generate_kid(keypair)
19
- super(keypair, kid)
20
+ @keypair = keypair
21
+
22
+ super(options)
20
23
  end
21
24
 
22
25
  def private?
23
26
  @keypair.private_key?
24
27
  end
25
28
 
26
- def export(options = {})
29
+ def members
27
30
  crv, x_octets, y_octets = keypair_components(keypair)
28
- exported_hash = {
31
+ {
29
32
  kty: KTY,
30
33
  crv: crv,
31
34
  x: encode_octets(x_octets),
32
- y: encode_octets(y_octets),
33
- kid: kid
35
+ y: encode_octets(y_octets)
34
36
  }
37
+ end
38
+
39
+ def export(options = {})
40
+ exported_hash = members.merge(kid: kid)
41
+
35
42
  return exported_hash unless private? && options[:include_private] == true
36
43
 
37
44
  append_private_parts(exported_hash)
38
45
  end
39
46
 
47
+ def key_digest
48
+ _crv, x_octets, y_octets = keypair_components(keypair)
49
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
50
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
51
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
52
+ end
53
+
40
54
  private
41
55
 
42
56
  def append_private_parts(the_hash)
@@ -46,13 +60,6 @@ module JWT
46
60
  )
47
61
  end
48
62
 
49
- def generate_kid(ec_keypair)
50
- _crv, x_octets, y_octets = keypair_components(ec_keypair)
51
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
- OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
- OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
54
- end
55
-
56
63
  def keypair_components(ec_keypair)
57
64
  encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
65
  case ec_keypair.group.curve_name
@@ -75,11 +82,11 @@ module JWT
75
82
  end
76
83
 
77
84
  def encode_octets(octets)
78
- Base64.urlsafe_encode64(octets, padding: false)
85
+ ::JWT::Base64.url_encode(octets)
79
86
  end
80
87
 
81
88
  def encode_open_ssl_bn(key_part)
82
- Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
89
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
83
90
  end
84
91
 
85
92
  class << self
@@ -90,7 +97,7 @@ module JWT
90
97
  jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
91
98
  raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
92
99
 
93
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
100
+ new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
94
101
  end
95
102
 
96
103
  def to_openssl_curve(crv)
@@ -114,39 +121,77 @@ module JWT
114
121
  end
115
122
  end
116
123
 
117
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
118
- curve = to_openssl_curve(jwk_crv)
119
-
120
- x_octets = decode_octets(jwk_x)
121
- y_octets = decode_octets(jwk_y)
122
-
123
- key = OpenSSL::PKey::EC.new(curve)
124
-
125
- # The details of the `Point` instantiation are covered in:
126
- # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
127
- # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
128
- # - https://tools.ietf.org/html/rfc5480#section-2.2
129
- # - https://www.secg.org/SEC1-Ver-1.0.pdf
130
- # Section 2.3.3 of the last of these references specifies that the
131
- # encoding of an uncompressed point consists of the byte `0x04` followed
132
- # by the x value then the y value.
133
- point = OpenSSL::PKey::EC::Point.new(
134
- OpenSSL::PKey::EC::Group.new(curve),
135
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
136
- )
137
-
138
- key.public_key = point
139
- key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
140
-
141
- key
124
+ if ::JWT.openssl_3?
125
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
126
+ curve = to_openssl_curve(jwk_crv)
127
+
128
+ x_octets = decode_octets(jwk_x)
129
+ y_octets = decode_octets(jwk_y)
130
+
131
+ point = OpenSSL::PKey::EC::Point.new(
132
+ OpenSSL::PKey::EC::Group.new(curve),
133
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
134
+ )
135
+
136
+ sequence = if jwk_d
137
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
138
+ # ECPrivateKey ::= SEQUENCE {
139
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
140
+ # privateKey OCTET STRING,
141
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
142
+ # publicKey [1] BIT STRING OPTIONAL
143
+ # }
144
+
145
+ OpenSSL::ASN1::Sequence([
146
+ OpenSSL::ASN1::Integer(1),
147
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
148
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
149
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
150
+ ])
151
+ else
152
+ OpenSSL::ASN1::Sequence([
153
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
154
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
155
+ ])
156
+ end
157
+
158
+ OpenSSL::PKey::EC.new(sequence.to_der)
159
+ end
160
+ else
161
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
162
+ curve = to_openssl_curve(jwk_crv)
163
+
164
+ x_octets = decode_octets(jwk_x)
165
+ y_octets = decode_octets(jwk_y)
166
+
167
+ key = OpenSSL::PKey::EC.new(curve)
168
+
169
+ # The details of the `Point` instantiation are covered in:
170
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
171
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
172
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
173
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
174
+ # Section 2.3.3 of the last of these references specifies that the
175
+ # encoding of an uncompressed point consists of the byte `0x04` followed
176
+ # by the x value then the y value.
177
+ point = OpenSSL::PKey::EC::Point.new(
178
+ OpenSSL::PKey::EC::Group.new(curve),
179
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
180
+ )
181
+
182
+ key.public_key = point
183
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
184
+
185
+ key
186
+ end
142
187
  end
143
188
 
144
189
  def decode_octets(jwk_data)
145
- Base64.urlsafe_decode64(jwk_data)
190
+ ::JWT::Base64.url_decode(jwk_data)
146
191
  end
147
192
 
148
193
  def decode_open_ssl_bn(jwk_data)
149
- OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
194
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
150
195
  end
151
196
  end
152
197
  end
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -3,14 +3,16 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class HMAC < KeyBase
6
- KTY = 'oct'
6
+ KTY = 'oct'
7
7
  KTYS = [KTY, String].freeze
8
8
 
9
- def initialize(keypair, kid = nil)
10
- raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
9
+ attr_reader :signing_key
11
10
 
12
- super
13
- @kid = kid || generate_kid
11
+ def initialize(signing_key, options = {})
12
+ raise ArgumentError, 'signing_key must be of type String' unless signing_key.is_a?(String)
13
+
14
+ @signing_key = signing_key
15
+ super(options)
14
16
  end
15
17
 
16
18
  def private?
@@ -31,14 +33,21 @@ module JWT
31
33
  return exported_hash unless private? && options[:include_private] == true
32
34
 
33
35
  exported_hash.merge(
34
- k: keypair
36
+ k: signing_key
35
37
  )
36
38
  end
37
39
 
38
- private
40
+ def members
41
+ {
42
+ kty: KTY,
43
+ k: signing_key
44
+ }
45
+ end
46
+
47
+ alias keypair signing_key # for backwards compatibility
39
48
 
40
- def generate_kid
41
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
49
+ def key_digest
50
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
42
51
  OpenSSL::ASN1::UTF8String.new(KTY)])
43
52
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
53
  end
@@ -50,7 +59,7 @@ module JWT
50
59
 
51
60
  raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
52
61
 
53
- new(jwk_k, jwk_kid)
62
+ new(jwk_k, kid: jwk_kid)
54
63
  end
55
64
  end
56
65
  end
@@ -3,17 +3,33 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class KeyBase
6
- attr_reader :keypair, :kid
7
-
8
- def initialize(keypair, kid = nil)
9
- @keypair = keypair
10
- @kid = kid
11
- end
12
-
13
6
  def self.inherited(klass)
14
7
  super
15
8
  ::JWT::JWK.classes << klass
16
9
  end
10
+
11
+ def initialize(options)
12
+ options ||= {}
13
+
14
+ if options.is_a?(String) # For backwards compatibility when kid was a String
15
+ options = { kid: options }
16
+ end
17
+
18
+ @kid = options[:kid]
19
+ @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
20
+ end
21
+
22
+ def kid
23
+ @kid ||= generate_kid
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :kid_generator
29
+
30
+ def generate_kid
31
+ kid_generator.new(self).generate
32
+ end
17
33
  end
18
34
  end
19
35
  end
@@ -28,7 +28,7 @@ module JWT
28
28
  return jwk if jwk
29
29
 
30
30
  if reloadable?
31
- load_keys(invalidate: true)
31
+ load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility
32
32
  return find_key(kid)
33
33
  end
34
34
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KidAsKeyDigest
6
+ def initialize(jwk)
7
+ @jwk = jwk
8
+ end
9
+
10
+ def generate
11
+ @jwk.key_digest
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -8,10 +8,14 @@ module JWT
8
8
  KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
9
  RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
10
 
11
- def initialize(keypair, kid = nil)
11
+ attr_reader :keypair
12
+
13
+ def initialize(keypair, options = {})
12
14
  raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
15
 
14
- super(keypair, kid || generate_kid(keypair.public_key))
16
+ @keypair = keypair
17
+
18
+ super(options)
15
19
  end
16
20
 
17
21
  def private?
@@ -23,26 +27,29 @@ module JWT
23
27
  end
24
28
 
25
29
  def export(options = {})
26
- exported_hash = {
27
- kty: KTY,
28
- n: encode_open_ssl_bn(public_key.n),
29
- e: encode_open_ssl_bn(public_key.e),
30
- kid: kid
31
- }
30
+ exported_hash = members.merge(kid: kid)
32
31
 
33
32
  return exported_hash unless private? && options[:include_private] == true
34
33
 
35
34
  append_private_parts(exported_hash)
36
35
  end
37
36
 
38
- private
37
+ def members
38
+ {
39
+ kty: KTY,
40
+ n: encode_open_ssl_bn(public_key.n),
41
+ e: encode_open_ssl_bn(public_key.e)
42
+ }
43
+ end
39
44
 
40
- def generate_kid(public_key)
45
+ def key_digest
41
46
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
42
47
  OpenSSL::ASN1::Integer.new(public_key.e)])
43
48
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
49
  end
45
50
 
51
+ private
52
+
46
53
  def append_private_parts(the_hash)
47
54
  the_hash.merge(
48
55
  d: encode_open_ssl_bn(keypair.d),
@@ -55,7 +62,7 @@ module JWT
55
62
  end
56
63
 
57
64
  def encode_open_ssl_bn(key_part)
58
- Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
65
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
59
66
  end
60
67
 
61
68
  class << self
@@ -63,8 +70,7 @@ module JWT
63
70
  pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
64
71
  decode_open_ssl_bn(value)
65
72
  end
66
- kid = jwk_attributes(jwk_data, :kid)[:kid]
67
- new(rsa_pkey(pkey_params), kid)
73
+ new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
68
74
  end
69
75
 
70
76
  private
@@ -80,35 +86,51 @@ module JWT
80
86
  def rsa_pkey(rsa_parameters)
81
87
  raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
82
88
 
83
- populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
89
+ create_rsa_key(rsa_parameters)
84
90
  end
85
91
 
86
- if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
87
- def populate_key(rsa_key, rsa_parameters)
88
- rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
89
- rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
90
- rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
91
- rsa_key
92
+ if ::JWT.openssl_3?
93
+ ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
94
+ def create_rsa_key(rsa_parameters)
95
+ sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
96
+ next if rsa_parameters[key].nil?
97
+
98
+ arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
99
+ end
100
+
101
+ if sequence.size > 2 # For a private key
102
+ sequence.unshift(OpenSSL::ASN1::Integer.new(0))
103
+ end
104
+
105
+ OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
106
+ end
107
+ elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
108
+ def create_rsa_key(rsa_parameters)
109
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
110
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
111
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
112
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
113
+ end
92
114
  end
93
115
  else
94
- def populate_key(rsa_key, rsa_parameters)
95
- rsa_key.n = rsa_parameters[:n]
96
- rsa_key.e = rsa_parameters[:e]
97
- rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
98
- rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
99
- rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
100
- rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
101
- rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
102
- rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
103
-
104
- rsa_key
116
+ def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
117
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
118
+ rsa_key.n = rsa_parameters[:n]
119
+ rsa_key.e = rsa_parameters[:e]
120
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
121
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
122
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
123
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
124
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
125
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
126
+ end
105
127
  end
106
128
  end
107
129
 
108
130
  def decode_open_ssl_bn(jwk_data)
109
131
  return nil unless jwk_data
110
132
 
111
- OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
133
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
112
134
  end
113
135
  end
114
136
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # https://tools.ietf.org/html/rfc7638
6
+ class Thumbprint
7
+ attr_reader :jwk
8
+
9
+ def initialize(jwk)
10
+ @jwk = jwk
11
+ end
12
+
13
+ def generate
14
+ ::Base64.urlsafe_encode64(
15
+ Digest::SHA256.digest(
16
+ JWT::JSON.generate(
17
+ jwk.members.sort.to_h
18
+ )
19
+ ), padding: false
20
+ )
21
+ end
22
+
23
+ alias to_s generate
24
+ end
25
+ end
26
+ end
data/lib/jwt/version.rb CHANGED
@@ -11,13 +11,18 @@ module JWT
11
11
  # major version
12
12
  MAJOR = 2
13
13
  # minor version
14
- MINOR = 4
14
+ MINOR = 5
15
15
  # tiny version
16
- TINY = 1
16
+ TINY = 0
17
17
  # alpha, beta, etc. tag
18
18
  PRE = nil
19
19
 
20
20
  # Build version string
21
21
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
22
22
  end
23
+
24
+ def self.openssl_3?
25
+ return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL')
26
+ return true if OpenSSL::OPENSSL_VERSION_NUMBER >= 3 * 0x10000000
27
+ end
23
28
  end
@@ -47,7 +47,7 @@ module JWT
47
47
  x5c_header_or_certificates
48
48
  else
49
49
  x5c_header_or_certificates.map do |encoded|
50
- OpenSSL::X509::Certificate.new(::Base64.strict_decode64(encoded))
50
+ OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded))
51
51
  end
52
52
  end
53
53
  end
data/lib/jwt.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
3
+ require 'jwt/version'
4
+ require 'jwt/base64'
4
5
  require 'jwt/json'
5
6
  require 'jwt/decode'
6
- require 'jwt/default_options'
7
+ require 'jwt/configuration'
7
8
  require 'jwt/encode'
8
9
  require 'jwt/error'
9
10
  require 'jwt/jwk'
@@ -13,7 +14,7 @@ require 'jwt/jwk'
13
14
  # Should be up to date with the latest spec:
14
15
  # https://tools.ietf.org/html/rfc7519
15
16
  module JWT
16
- include JWT::DefaultOptions
17
+ extend ::JWT::Configuration
17
18
 
18
19
  module_function
19
20
 
@@ -25,6 +26,6 @@ module JWT
25
26
  end
26
27
 
27
28
  def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter
28
- Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments
29
+ Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments
29
30
  end
30
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-07 00:00:00.000000000 Z
11
+ date: 2022-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -127,9 +127,13 @@ files:
127
127
  - lib/jwt/algos/ps.rb
128
128
  - lib/jwt/algos/rsa.rb
129
129
  - lib/jwt/algos/unsupported.rb
130
+ - lib/jwt/base64.rb
130
131
  - lib/jwt/claims_validator.rb
132
+ - lib/jwt/configuration.rb
133
+ - lib/jwt/configuration/container.rb
134
+ - lib/jwt/configuration/decode_configuration.rb
135
+ - lib/jwt/configuration/jwk_configuration.rb
131
136
  - lib/jwt/decode.rb
132
- - lib/jwt/default_options.rb
133
137
  - lib/jwt/encode.rb
134
138
  - lib/jwt/error.rb
135
139
  - lib/jwt/json.rb
@@ -138,7 +142,9 @@ files:
138
142
  - lib/jwt/jwk/hmac.rb
139
143
  - lib/jwt/jwk/key_base.rb
140
144
  - lib/jwt/jwk/key_finder.rb
145
+ - lib/jwt/jwk/kid_as_key_digest.rb
141
146
  - lib/jwt/jwk/rsa.rb
147
+ - lib/jwt/jwk/thumbprint.rb
142
148
  - lib/jwt/security_utils.rb
143
149
  - lib/jwt/signature.rb
144
150
  - lib/jwt/verify.rb
@@ -150,7 +156,7 @@ licenses:
150
156
  - MIT
151
157
  metadata:
152
158
  bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
153
- changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.4.1/CHANGELOG.md
159
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.5.0/CHANGELOG.md
154
160
  post_install_message:
155
161
  rdoc_options: []
156
162
  require_paths:
@@ -166,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
172
  - !ruby/object:Gem::Version
167
173
  version: '0'
168
174
  requirements: []
169
- rubygems_version: 3.3.7
175
+ rubygems_version: 3.3.21
170
176
  signing_key:
171
177
  specification_version: 4
172
178
  summary: JSON Web Token implementation in Ruby
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JWT
4
- module DefaultOptions
5
- DEFAULT_OPTIONS = {
6
- verify_expiration: true,
7
- verify_not_before: true,
8
- verify_iss: false,
9
- verify_iat: false,
10
- verify_jti: false,
11
- verify_aud: false,
12
- verify_sub: false,
13
- leeway: 0,
14
- algorithms: ['HS256'],
15
- required_claims: []
16
- }.freeze
17
- end
18
- end