jwt 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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