jwt 3.0.0 → 3.1.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: 8bc851245c9a662d91209f426033dc2b2521de4cd07f11529aa34c0e69ad9201
4
- data.tar.gz: fa9ae48864cb04df26a6e003e90430b1dcf85ad4ba1da707ea688b6efd43b9b4
3
+ metadata.gz: a9dd430911612a1bef370bffdf46f432a9ec2027f2091efd8d47e6d237d8c935
4
+ data.tar.gz: 6fa7001bc60edeb8c984cddef3bf5d9beb42656555593ad8ffc2dfe3ef4b76e8
5
5
  SHA512:
6
- metadata.gz: 26e15ad0fa03a06b830b380d74b2efbc23a85ecdc8f3a72b1d938706622c83d28217167e87eec73c7633dfc0ee3656c0c2d2ba4fd604cedb1289cc36d730706c
7
- data.tar.gz: 0076ad48c7dcc3ddc3fa55b776ca4566452c17aae211af36616634429e37fd687fb29fcf5e0fea5ee32945fe2359784f2ee48535a76509f90813dfe481892394
6
+ metadata.gz: 11a6a56acb50a86223e7f60f413ac723c34997bd7ad5f2bbf2376e6c66fbfca1991a08301147062e7d5835f8fd530509b53a80876781d890d52e7fab7389fe18
7
+ data.tar.gz: 44f001cb3c746e4fe502a72fa9bc9234066a687145443c14cd4b09ef2bd184d119bf0d99accbfd3cd748e0eb1e8696fd90446cc3be4851ead641adbba9246b7c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.1.0](https://github.com/jwt/ruby-jwt/tree/v3.1.0) (2025-06-23)
4
+
5
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.0.0...v3.1.0)
6
+
7
+ **Features:**
8
+
9
+ - Add support for x5t header parameter for X.509 certificate thumbprint verification [#669](https://github.com/jwt/ruby-jwt/pull/669) ([@hieuk09](https://github.com/hieuk09))
10
+ - Raise an error if the ECDSA signing or verification key is not an instance of `OpenSSL::PKey::EC` [#688](https://github.com/jwt/ruby-jwt/pull/688) ([@anakinj](https://github.com/anakinj))
11
+ - Allow `OpenSSL::PKey::EC::Point` to be used as the verification key in ECDSA [#689](https://github.com/jwt/ruby-jwt/pull/689) ([@anakinj](https://github.com/anakinj))
12
+ - Require claims to have been verified before accessing the `JWT::EncodedToken#payload` [#690](https://github.com/jwt/ruby-jwt/pull/690) ([@anakinj](https://github.com/anakinj))
13
+ - Support signing and verifying tokens using a JWK [#692](https://github.com/jwt/ruby-jwt/pull/692) ([@anakinj](https://github.com/anakinj))
14
+
3
15
  ## [v3.0.0](https://github.com/jwt/ruby-jwt/tree/v3.0.0) (2025-06-14)
4
16
 
5
17
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.1...v3.0.0)
data/README.md CHANGED
@@ -9,11 +9,7 @@ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools
9
9
 
10
10
  If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
11
11
 
12
- See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
13
-
14
- ## Upcoming breaking changes
15
-
16
- Check out breaking changes in the upcoming **version 3.0** from the [upgrade guide](UPGRADING.md)
12
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes and [upgrade guide](UPGRADING.md) for upgrading between major versions.
17
13
 
18
14
  ## Sponsors
19
15
 
@@ -60,24 +56,15 @@ See [JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values fo
60
56
  - none - unsigned token
61
57
 
62
58
  ```ruby
63
-
64
59
  payload = { data: 'test' }
60
+ token = JWT.encode(payload, nil, 'none')
61
+ # => "eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9."
65
62
 
66
- # IMPORTANT: set nil as password parameter
67
- token = JWT.encode(payload, nil, 'none')
68
-
69
- # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
70
- puts token
71
-
72
- # Set password to nil and validation to false otherwise this won't work
73
- decoded_token = JWT.decode(token, nil, false)
74
-
75
- # Array
76
- # [
77
- # {"data"=>"test"}, # payload
78
- # {"alg"=>"none"} # header
79
- # ]
80
- puts decoded_token
63
+ decoded_token = JWT.decode(token, nil, true, { algorithm: 'none' })
64
+ # => [
65
+ # {"data"=>"test"}, # payload
66
+ # {"alg"=>"none"} # header
67
+ # ]
81
68
  ```
82
69
 
83
70
  ### **HMAC**
@@ -87,22 +74,17 @@ puts decoded_token
87
74
  - HS512 - HMAC using SHA-512 hash algorithm
88
75
 
89
76
  ```ruby
90
- # The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided.
77
+ payload = { data: 'test' }
91
78
  hmac_secret = 'my$ecretK3y'
92
79
 
93
80
  token = JWT.encode(payload, hmac_secret, 'HS256')
94
-
95
- # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
96
- puts token
81
+ # => "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y"
97
82
 
98
83
  decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
99
-
100
- # Array
101
- # [
102
- # {"data"=>"test"}, # payload
103
- # {"alg"=>"HS256"} # header
104
- # ]
105
- puts decoded_token
84
+ # => [
85
+ # {"data"=>"test"}, # payload
86
+ # {"alg"=>"HS256"} # header
87
+ # ]
106
88
  ```
107
89
 
108
90
  ### **RSA**
@@ -112,22 +94,18 @@ puts decoded_token
112
94
  - RS512 - RSA using SHA-512 hash algorithm
113
95
 
114
96
  ```ruby
97
+ payload = { data: 'test' }
115
98
  rsa_private = OpenSSL::PKey::RSA.generate(2048)
116
- rsa_public = rsa_private.public_key
99
+ rsa_public = rsa_private.public_key
117
100
 
118
101
  token = JWT.encode(payload, rsa_private, 'RS256')
119
-
120
- # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
121
- puts token
102
+ # => "eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.CCkO35qFPijW8Gwhbt8a80PB9fc9FJ19hCMnXSgoDF6Mlvlt0A4G-ah..."
122
103
 
123
104
  decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
124
-
125
- # Array
126
- # [
127
- # {"data"=>"test"}, # payload
128
- # {"alg"=>"RS256"} # header
129
- # ]
130
- puts decoded_token
105
+ # => [
106
+ # {"data"=>"test"}, # payload
107
+ # {"alg"=>"RS256"} # header
108
+ # ]
131
109
  ```
132
110
 
133
111
  ### **ECDSA**
@@ -138,26 +116,22 @@ puts decoded_token
138
116
  - ES256K - ECDSA using P-256K and SHA-256
139
117
 
140
118
  ```ruby
119
+ payload = { data: 'test' }
141
120
  ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
142
121
 
143
122
  token = JWT.encode(payload, ecdsa_key, 'ES256')
144
-
145
- # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
146
- puts token
123
+ # => "eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg"
147
124
 
148
125
  decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' })
149
-
150
- # Array
151
- # [
152
- # {"test"=>"data"}, # payload
153
- # {"alg"=>"ES256"} # header
154
- # ]
155
- puts decoded_token
126
+ # => [
127
+ # {"test"=>"data"}, # payload
128
+ # {"alg"=>"ES256"} # header
129
+ # ]
156
130
  ```
157
131
 
158
132
  ### **EdDSA**
159
133
 
160
- This algorithm has since version 3.0 been moved to the [jwt-eddsa gem](https://rubygems.org/gems/jwt-eddsa).
134
+ Since version 3.0, the EdDSA algorithm has been moved to the [jwt-eddsa gem](https://rubygems.org/gems/jwt-eddsa).
161
135
 
162
136
  ### **RSASSA-PSS**
163
137
 
@@ -166,22 +140,18 @@ This algorithm has since version 3.0 been moved to the [jwt-eddsa gem](https://r
166
140
  - PS512 - RSASSA-PSS using SHA-512 hash algorithm
167
141
 
168
142
  ```ruby
143
+ payload = { data: 'test' }
169
144
  rsa_private = OpenSSL::PKey::RSA.generate(2048)
170
- rsa_public = rsa_private.public_key
145
+ rsa_public = rsa_private.public_key
171
146
 
172
147
  token = JWT.encode(payload, rsa_private, 'PS256')
173
-
174
- # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
175
- puts token
148
+ # => "eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.BRWizdUjD5zAWw-EDBcrl3dDpQDAePz9Ol3XKC43SggU47G8OWwveA_..."
176
149
 
177
150
  decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
178
-
179
- # Array
180
- # [
181
- # {"data"=>"test"}, # payload
182
- # {"alg"=>"PS256"} # header
183
- # ]
184
- puts decoded_token
151
+ # => [
152
+ # {"data"=>"test"}, # payload
153
+ # {"alg"=>"PS256"} # header
154
+ # ]
185
155
  ```
186
156
 
187
157
  ### **Custom algorithms**
@@ -210,8 +180,15 @@ module CustomHS512Algorithm
210
180
  end
211
181
  end
212
182
 
213
- token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
214
- payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
183
+ payload = { data: 'test' }
184
+ token = JWT.encode(payload, 'secret', CustomHS512Algorithm)
185
+ # => "eyJhbGciOiJIUzUxMiJ9.eyJkYXRhIjoidGVzdCJ9.aBNoejLEM2WMF3TxzRDKlehYdG2ATvFpGNauTI4GSD2VJseS_sC8covrVMlgslf0aJM4SKb3EIeORJBFPtZ33w"
186
+
187
+ decoded_token = JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
188
+ # => [
189
+ # {"data"=>"test"}, # payload
190
+ # {"alg"=>"HS512"} # header
191
+ # ]
215
192
  ```
216
193
 
217
194
  ### Add custom header fields
@@ -220,30 +197,16 @@ The ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc
220
197
  To add custom header fields you need to pass `header_fields` parameter
221
198
 
222
199
  ```ruby
223
- token = JWT.encode(payload, key, 'HS256', {})
224
- ```
225
-
226
- **Example:**
227
-
228
- ```ruby
229
-
230
200
  payload = { data: 'test' }
231
201
 
232
- # IMPORTANT: set nil as password parameter
233
202
  token = JWT.encode(payload, nil, 'none', { typ: 'JWT' })
203
+ # => "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9."
234
204
 
235
- # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
236
- puts token
237
-
238
- # Set password to nil and validation to false otherwise this won't work
239
- decoded_token = JWT.decode(token, nil, false)
240
-
241
- # Array
242
- # [
243
- # {"data"=>"test"}, # payload
244
- # {"typ"=>"JWT", "alg"=>"none"} # header
245
- # ]
246
- puts decoded_token
205
+ decoded_token = JWT.decode(token, nil, true, { algorithm: 'none' })
206
+ # => [
207
+ # {"data"=>"test"}, # payload
208
+ # {"typ"=>"JWT", "alg"=>"none"} # header
209
+ # ]
247
210
  ```
248
211
 
249
212
  ## `JWT::Token` and `JWT::EncodedToken`
@@ -253,10 +216,14 @@ The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs
253
216
  ### Signing and encoding a token
254
217
 
255
218
  ```ruby
256
- token = JWT::Token.new(payload: { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }, header: { kid: 'hmac' })
219
+ payload = { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }
220
+ header = { kid: 'hmac' }
221
+
222
+ token = JWT::Token.new(payload: payload, header: header)
257
223
  token.sign!(algorithm: 'HS256', key: "secret")
258
224
 
259
- token.jwt # => "eyJhbGciOiJIUzI1N..."
225
+ token.jwt
226
+ # => "eyJraWQiOiJobWFjIiwiYWxnIjoiSFMyNTYifQ.eyJleHAiOjE3NTAwMDU0NzksImp0aSI6IjEyMzQiLCJzdWIiOiJteS1zdWJqZWN0In0.NRLcK6fYr3IdNfmncJePMWLQ34M4n14EgqSYrQIjL9w"
260
227
  ```
261
228
 
262
229
  ### Verifying and decoding a token
@@ -284,7 +251,27 @@ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
284
251
  encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
285
252
  ```
286
253
 
287
- #### Keyfinders
254
+ A JWK can be used to sign and verify the token if it's possible to derive the signing algorithm from the key.
255
+
256
+ ```ruby
257
+ jwk_json = '{
258
+ "kty": "oct",
259
+ "k": "c2VjcmV0",
260
+ "alg": "HS256",
261
+ "kid": "hmac"
262
+ }'
263
+
264
+ jwk = JWT::JWK.import(JSON.parse(jwk_json))
265
+
266
+ token = JWT::Token.new(payload: payload, header: header)
267
+
268
+ token.sign!(key: jwk)
269
+
270
+ encoded_token = JWT::EncodedToken.new(token.jwt)
271
+ encoded_token.verify!(signature: { key: jwk})
272
+ ```
273
+
274
+ #### Using a keyfinder
288
275
 
289
276
  A keyfinder can be used to verify a signature. A keyfinder is an object responding to the `#call` method. The method expects to receive one argument, which is the token to be verified.
290
277
 
@@ -677,13 +664,14 @@ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
677
664
  JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
678
665
  ```
679
666
 
680
- The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
667
+ The `jwks` option can also be given as a lambda that evaluates every time a key identifier is resolved.
681
668
  This can be used to implement caching of remotely fetched JWK Sets.
682
669
 
683
- If the requested `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`.
670
+ Key identifiers can be specified using `kid`, `x5t` header parameters.
671
+ If the requested identifier is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`.
684
672
  The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
685
673
 
686
- Tokens without a specified `kid` are rejected by default.
674
+ Tokens without a specified key identifier (`kid` or `x5t`) are rejected by default.
687
675
  This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`.
688
676
 
689
677
  ```ruby
data/lib/jwt/decode.rb CHANGED
@@ -65,7 +65,14 @@ module JWT
65
65
 
66
66
  def set_key
67
67
  @key = find_key(&@keyfinder) if @keyfinder
68
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
68
+ if @options[:jwks]
69
+ @key = ::JWT::JWK::KeyFinder.new(
70
+ jwks: @options[:jwks],
71
+ allow_nil_kid: @options[:allow_nil_kid],
72
+ key_fields: @options[:key_fields]
73
+ ).call(token)
74
+ end
75
+
69
76
  return unless (x5c_options = @options[:x5c])
70
77
 
71
78
  @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
@@ -28,6 +28,10 @@ module JWT
28
28
  end
29
29
  end
30
30
 
31
+ DEFAULT_CLAIMS = [:exp].freeze
32
+
33
+ private_constant(:DEFAULT_CLAIMS)
34
+
31
35
  # Returns the original token provided to the class.
32
36
  # @return [String] The JWT token.
33
37
  attr_reader :jwt
@@ -41,6 +45,8 @@ module JWT
41
45
 
42
46
  @jwt = jwt
43
47
  @signature_verified = false
48
+ @claims_verified = false
49
+
44
50
  @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
45
51
  end
46
52
 
@@ -68,12 +74,13 @@ module JWT
68
74
  # @return [String] the encoded header.
69
75
  attr_reader :encoded_header
70
76
 
71
- # Returns the payload of the JWT token. Access requires the signature to have been verified.
77
+ # Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
72
78
  #
73
79
  # @return [Hash] the payload.
74
80
  # @raise [JWT::DecodeError] if the signature has not been verified.
75
81
  def payload
76
82
  raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
83
+ raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified
77
84
 
78
85
  decoded_payload
79
86
  end
@@ -106,12 +113,23 @@ module JWT
106
113
  # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
107
114
  # @return [nil]
108
115
  # @raise [JWT::DecodeError] if the signature or claim verification fails.
109
- def verify!(signature:, claims: [:exp])
116
+ def verify!(signature:, claims: nil)
110
117
  verify_signature!(**signature)
111
118
  claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
112
119
  nil
113
120
  end
114
121
 
122
+ # Verifies the token signature and claims.
123
+ # By default it verifies the 'exp' claim.
124
+
125
+ # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
126
+ # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
127
+ # @return [Boolean] true if the signature and claims are valid, false otherwise.
128
+ def valid?(signature:, claims: nil)
129
+ valid_signature?(**signature) &&
130
+ (claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
131
+ end
132
+
115
133
  # Verifies the signature of the JWT token.
116
134
  #
117
135
  # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
@@ -120,12 +138,8 @@ module JWT
120
138
  # @return [nil]
121
139
  # @raise [JWT::VerificationError] if the signature verification fails.
122
140
  # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
123
- def verify_signature!(algorithm:, key: nil, key_finder: nil)
124
- raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
125
-
126
- key ||= key_finder.call(self)
127
-
128
- return if valid_signature?(algorithm: algorithm, key: key)
141
+ def verify_signature!(algorithm: nil, key: nil, key_finder: nil)
142
+ return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder)
129
143
 
130
144
  raise JWT::VerificationError, 'Signature verification failed'
131
145
  end
@@ -133,43 +147,59 @@ module JWT
133
147
  # Checks if the signature of the JWT token is valid.
134
148
  #
135
149
  # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
136
- # @param key [String, Array<String>] the key(s) to use for verification.
150
+ # @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
151
+ # @param key_finder [#call] an object responding to `call` to find the key for verification.
137
152
  # @return [Boolean] true if the signature is valid, false otherwise.
138
- def valid_signature?(algorithm:, key:)
139
- valid = Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
140
- Array(key).any? do |one_key|
141
- algo.verify(data: signing_input, signature: signature, verification_key: one_key)
142
- end
143
- end
153
+ def valid_signature?(algorithm: nil, key: nil, key_finder: nil)
154
+ raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
155
+
156
+ keys = Array(key || key_finder.call(self))
157
+ verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg'])
144
158
 
159
+ raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty?
160
+
161
+ valid = verifiers.any? do |jwa|
162
+ jwa.verify(data: signing_input, signature: signature)
163
+ end
145
164
  valid.tap { |verified| @signature_verified = verified }
146
165
  end
147
166
 
148
167
  # Verifies the claims of the token.
149
- # @param options [Array<Symbol>, Hash] the claims to verify.
168
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
150
169
  # @raise [JWT::DecodeError] if the claims are invalid.
151
170
  def verify_claims!(*options)
152
- Claims::Verifier.verify!(ClaimsContext.new(self), *options)
171
+ Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
172
+ @claims_verified = true
173
+ end
174
+ rescue StandardError
175
+ @claims_verified = false
176
+ raise
153
177
  end
154
178
 
155
179
  # Returns the errors of the claims of the token.
156
- # @param options [Array<Symbol>, Hash] the claims to verify.
180
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
157
181
  # @return [Array<Symbol>] the errors of the claims.
158
182
  def claim_errors(*options)
159
- Claims::Verifier.errors(ClaimsContext.new(self), *options)
183
+ Claims::Verifier.errors(ClaimsContext.new(self), *claims_options(options))
160
184
  end
161
185
 
162
186
  # Returns whether the claims of the token are valid.
163
- # @param options [Array<Symbol>, Hash] the claims to verify.
187
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
164
188
  # @return [Boolean] whether the claims are valid.
165
189
  def valid_claims?(*options)
166
- claim_errors(*options).empty?
190
+ claim_errors(*claims_options(options)).empty?.tap { |verified| @claims_verified = verified }
167
191
  end
168
192
 
169
193
  alias to_s jwt
170
194
 
171
195
  private
172
196
 
197
+ def claims_options(options)
198
+ return DEFAULT_CLAIMS if options.first.nil?
199
+
200
+ options
201
+ end
202
+
173
203
  def decode_payload
174
204
  raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
175
205
 
data/lib/jwt/jwa/ecdsa.rb CHANGED
@@ -12,14 +12,22 @@ module JWT
12
12
  end
13
13
 
14
14
  def sign(data:, signing_key:)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC)
16
+ raise_sign_error!('The given key is not a private key') unless signing_key.private?
17
+
15
18
  curve_definition = curve_by_name(signing_key.group.curve_name)
16
19
  key_algorithm = curve_definition[:algorithm]
20
+
17
21
  raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
18
22
 
19
23
  asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
20
24
  end
21
25
 
22
26
  def verify(data:, signature:, verification_key:)
27
+ verification_key = self.class.create_public_key_from_point(verification_key) if verification_key.is_a?(::OpenSSL::PKey::EC::Point)
28
+
29
+ raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::EC instance") unless verification_key.is_a?(::OpenSSL::PKey::EC)
30
+
23
31
  curve_definition = curve_by_name(verification_key.group.curve_name)
24
32
  key_algorithm = curve_definition[:algorithm]
25
33
  raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
@@ -56,12 +64,29 @@ module JWT
56
64
  register_algorithm(new(v[:algorithm], v[:digest]))
57
65
  end
58
66
 
67
+ # @api private
59
68
  def self.curve_by_name(name)
60
69
  NAMED_CURVES.fetch(name) do
61
70
  raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
62
71
  end
63
72
  end
64
73
 
74
+ if ::JWT.openssl_3?
75
+ def self.create_public_key_from_point(point)
76
+ sequence = OpenSSL::ASN1::Sequence([
77
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(point.group.curve_name)]),
78
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
79
+ ])
80
+ OpenSSL::PKey::EC.new(sequence.to_der)
81
+ end
82
+ else
83
+ def self.create_public_key_from_point(point)
84
+ OpenSSL::PKey::EC.new(point.group.curve_name).tap do |key|
85
+ key.public_key = point
86
+ end
87
+ end
88
+ end
89
+
65
90
  private
66
91
 
67
92
  attr_reader :digest
data/lib/jwt/jwa.rb CHANGED
@@ -13,11 +13,43 @@ require_relative 'jwa/unsupported'
13
13
  module JWT
14
14
  # The JWA module contains all supported algorithms.
15
15
  module JWA
16
+ # @api private
17
+ class VerifierContext
18
+ def initialize(jwa:, keys:)
19
+ @jwa = jwa
20
+ @keys = Array(keys)
21
+ end
22
+
23
+ def verify(*args, **kwargs)
24
+ @keys.any? do |key|
25
+ @jwa.verify(*args, **kwargs, verification_key: key)
26
+ end
27
+ end
28
+ end
29
+
30
+ # @api private
31
+ class SignerContext
32
+ def initialize(jwa:, key:)
33
+ @jwa = jwa
34
+ @key = key
35
+ end
36
+
37
+ def sign(*args, **kwargs)
38
+ @jwa.sign(*args, **kwargs, signing_key: @key)
39
+ end
40
+
41
+ def jwa_header
42
+ @jwa.header
43
+ end
44
+ end
45
+
16
46
  class << self
17
47
  # @api private
18
48
  def resolve(algorithm)
19
49
  return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
20
50
 
51
+ raise ArgumentError, 'Algorithm must be provided' if algorithm.nil?
52
+
21
53
  raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm)
22
54
 
23
55
  algorithm
@@ -25,8 +57,25 @@ module JWT
25
57
 
26
58
  # @api private
27
59
  def resolve_and_sort(algorithms:, preferred_algorithm:)
28
- algs = Array(algorithms).map { |alg| JWA.resolve(alg) }
29
- algs.partition { |alg| alg.valid_alg?(preferred_algorithm) }.flatten
60
+ Array(algorithms).map { |alg| JWA.resolve(alg) }
61
+ .partition { |alg| alg.valid_alg?(preferred_algorithm) }
62
+ .flatten
63
+ end
64
+
65
+ # @api private
66
+ def create_signer(algorithm:, key:)
67
+ return key if key.is_a?(JWK::KeyBase)
68
+
69
+ SignerContext.new(jwa: resolve(algorithm), key: key)
70
+ end
71
+
72
+ # @api private
73
+ def create_verifiers(algorithms:, keys:, preferred_algorithm:)
74
+ jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
75
+
76
+ jwks + resolve_and_sort(algorithms: algorithms,
77
+ preferred_algorithm: preferred_algorithm)
78
+ .map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
30
79
  end
31
80
  end
32
81
  end
data/lib/jwt/jwk/ec.rb CHANGED
@@ -73,6 +73,13 @@ module JWT
73
73
 
74
74
  private
75
75
 
76
+ def jwa
77
+ return super if self[:alg]
78
+
79
+ curve_name = self.class.to_openssl_curve(self[:crv])
80
+ JWA.resolve(JWA::Ecdsa.curve_by_name(curve_name)[:algorithm])
81
+ end
82
+
76
83
  def ec_key
77
84
  @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78
85
  end
@@ -136,67 +143,54 @@ module JWT
136
143
  }.compact
137
144
  end
138
145
 
139
- if ::JWT.openssl_3?
140
- def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
141
- curve = EC.to_openssl_curve(jwk_crv)
142
- x_octets = decode_octets(jwk_x)
143
- y_octets = decode_octets(jwk_y)
144
-
145
- point = OpenSSL::PKey::EC::Point.new(
146
- OpenSSL::PKey::EC::Group.new(curve),
147
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
148
- )
149
-
150
- sequence = if jwk_d
151
- # https://datatracker.ietf.org/doc/html/rfc5915.html
152
- # ECPrivateKey ::= SEQUENCE {
153
- # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
154
- # privateKey OCTET STRING,
155
- # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
156
- # publicKey [1] BIT STRING OPTIONAL
157
- # }
158
-
159
- OpenSSL::ASN1::Sequence([
160
- OpenSSL::ASN1::Integer(1),
161
- OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
162
- OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
163
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
164
- ])
165
- else
166
- OpenSSL::ASN1::Sequence([
167
- OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
168
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
169
- ])
170
- end
146
+ def create_point(jwk_crv, jwk_x, jwk_y)
147
+ curve = EC.to_openssl_curve(jwk_crv)
148
+ x_octets = decode_octets(jwk_x)
149
+ y_octets = decode_octets(jwk_y)
150
+
151
+ # The details of the `Point` instantiation are covered in:
152
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
153
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
154
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
155
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
156
+ # Section 2.3.3 of the last of these references specifies that the
157
+ # encoding of an uncompressed point consists of the byte `0x04` followed
158
+ # by the x value then the y value.
159
+ OpenSSL::PKey::EC::Point.new(
160
+ OpenSSL::PKey::EC::Group.new(curve),
161
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
162
+ )
163
+ end
171
164
 
165
+ if ::JWT.openssl_3?
166
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
167
+ point = create_point(jwk_crv, jwk_x, jwk_y)
168
+
169
+ return ::JWT::JWA::Ecdsa.create_public_key_from_point(point) unless jwk_d
170
+
171
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
172
+ # ECPrivateKey ::= SEQUENCE {
173
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
174
+ # privateKey OCTET STRING,
175
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
176
+ # publicKey [1] BIT STRING OPTIONAL
177
+ # }
178
+
179
+ sequence = OpenSSL::ASN1::Sequence([
180
+ OpenSSL::ASN1::Integer(1),
181
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
182
+ OpenSSL::ASN1::ObjectId(point.group.curve_name, 0, :EXPLICIT),
183
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
184
+ ])
172
185
  OpenSSL::PKey::EC.new(sequence.to_der)
173
186
  end
174
187
  else
175
188
  def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
176
- curve = EC.to_openssl_curve(jwk_crv)
177
-
178
- x_octets = decode_octets(jwk_x)
179
- y_octets = decode_octets(jwk_y)
180
-
181
- key = OpenSSL::PKey::EC.new(curve)
182
-
183
- # The details of the `Point` instantiation are covered in:
184
- # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
185
- # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
186
- # - https://tools.ietf.org/html/rfc5480#section-2.2
187
- # - https://www.secg.org/SEC1-Ver-1.0.pdf
188
- # Section 2.3.3 of the last of these references specifies that the
189
- # encoding of an uncompressed point consists of the byte `0x04` followed
190
- # by the x value then the y value.
191
- point = OpenSSL::PKey::EC::Point.new(
192
- OpenSSL::PKey::EC::Group.new(curve),
193
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
194
- )
195
-
196
- key.public_key = point
197
- key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
198
-
199
- key
189
+ point = create_point(jwk_crv, jwk_x, jwk_y)
190
+
191
+ ::JWT::JWA::Ecdsa.create_public_key_from_point(point).tap do |key|
192
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
193
+ end
200
194
  end
201
195
  end
202
196
 
@@ -205,7 +199,7 @@ module JWT
205
199
  # Some base64 encoders on some platform omit a single 0-byte at
206
200
  # the start of either Y or X coordinate of the elliptic curve point.
207
201
  # This leads to an encoding error when data is passed to OpenSSL BN.
208
- # It is know to have happend to exported JWKs on a Java application and
202
+ # It is know to have happened to exported JWKs on a Java application and
209
203
  # on a Flutter/Dart application (both iOS and Android). All that is
210
204
  # needed to fix the problem is adding a leading 0-byte. We know the
211
205
  # required byte is 0 because with any other byte the point is no longer
@@ -42,6 +42,19 @@ module JWT
42
42
  other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid]
43
43
  end
44
44
 
45
+ def verify(**kwargs)
46
+ jwa.verify(**kwargs, verification_key: verify_key)
47
+ end
48
+
49
+ def sign(**kwargs)
50
+ jwa.sign(**kwargs, signing_key: signing_key)
51
+ end
52
+
53
+ # @api private
54
+ def jwa_header
55
+ jwa.header
56
+ end
57
+
45
58
  alias eql? ==
46
59
 
47
60
  def <=>(other)
@@ -52,6 +65,12 @@ module JWT
52
65
 
53
66
  private
54
67
 
68
+ def jwa
69
+ raise JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing' unless self[:alg]
70
+
71
+ JWA.resolve(self[:alg])
72
+ end
73
+
55
74
  attr_reader :parameters
56
75
  end
57
76
  end
@@ -9,6 +9,9 @@ module JWT
9
9
  # @param [Hash] options the options to create a KeyFinder with
10
10
  # @option options [Proc, JWT::JWK::Set] :jwks the jwks or a loader proc
11
11
  # @option options [Boolean] :allow_nil_kid whether to allow nil kid
12
+ # @option options [Array] :key_fields the fields to use for key matching,
13
+ # the order of the fields are used to determine
14
+ # the priority of the keys.
12
15
  def initialize(options)
13
16
  @allow_nil_kid = options[:allow_nil_kid]
14
17
  jwks_or_loader = options[:jwks]
@@ -18,15 +21,16 @@ module JWT
18
21
  else
19
22
  ->(_options) { jwks_or_loader }
20
23
  end
24
+
25
+ @key_fields = options[:key_fields] || %i[kid]
21
26
  end
22
27
 
23
28
  # Returns the verification key for the given kid
24
29
  # @param [String] kid the key id
25
- def key_for(kid)
26
- raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
27
- raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
30
+ def key_for(kid, key_field = :kid)
31
+ raise ::JWT::DecodeError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)
28
32
 
29
- jwk = resolve_key(kid)
33
+ jwk = resolve_key(kid, key_field)
30
34
 
31
35
  raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
32
36
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
@@ -37,22 +41,31 @@ module JWT
37
41
  # Returns the key for the given token
38
42
  # @param [JWT::EncodedToken] token the token
39
43
  def call(token)
40
- key_for(token.header['kid'])
44
+ @key_fields.each do |key_field|
45
+ field_value = token.header[key_field.to_s]
46
+
47
+ return key_for(field_value, key_field) if field_value
48
+ end
49
+
50
+ raise ::JWT::DecodeError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid
51
+
52
+ kid = token.header['kid']
53
+ key_for(kid)
41
54
  end
42
55
 
43
56
  private
44
57
 
45
- def resolve_key(kid)
46
- key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
58
+ def resolve_key(kid, key_field)
59
+ key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[key_field] == kid }
47
60
 
48
61
  # First try without invalidation to facilitate application caching
49
- @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
62
+ @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(key_field => kid))
50
63
  jwk = @jwks.find { |key| key_matcher.call(key) }
51
64
 
52
65
  return jwk if jwk
53
66
 
54
67
  # Second try, invalidate for backwards compatibility
55
- @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
68
+ @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, key_field => kid))
56
69
  @jwks.find { |key| key_matcher.call(key) }
57
70
  end
58
71
  end
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -51,6 +51,7 @@ module JWT
51
51
  def export(options = {})
52
52
  exported = parameters.clone
53
53
  exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
54
+
54
55
  exported
55
56
  end
56
57
 
data/lib/jwt/token.rb CHANGED
@@ -87,16 +87,16 @@ module JWT
87
87
 
88
88
  # Signs the JWT token.
89
89
  #
90
+ # @param key [String, JWT::JWK::KeyBase] the key to use for signing.
90
91
  # @param algorithm [String, Object] the algorithm to use for signing.
91
- # @param key [String] the key to use for signing.
92
92
  # @return [void]
93
93
  # @raise [JWT::EncodeError] if the token is already signed or other problems when signing
94
- def sign!(algorithm:, key:)
94
+ def sign!(key:, algorithm: nil)
95
95
  raise ::JWT::EncodeError, 'Token already signed' if @signature
96
96
 
97
- JWA.resolve(algorithm).tap do |algo|
98
- header.merge!(algo.header) { |_key, old, _new| old }
99
- @signature = algo.sign(data: signing_input, signing_key: key)
97
+ JWA.create_signer(algorithm: algorithm, key: key).tap do |signer|
98
+ header.merge!(signer.jwa_header) { |_key, old, _new| old }
99
+ @signature = signer.sign(data: signing_input)
100
100
  end
101
101
 
102
102
  nil
data/lib/jwt/version.rb CHANGED
@@ -15,7 +15,7 @@ module JWT
15
15
  # Version constants
16
16
  module VERSION
17
17
  MAJOR = 3
18
- MINOR = 0
18
+ MINOR = 1
19
19
  TINY = 0
20
20
  PRE = nil
21
21
 
data/ruby-jwt.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
 
36
36
  spec.add_development_dependency 'appraisal'
37
37
  spec.add_development_dependency 'bundler'
38
+ spec.add_development_dependency 'irb'
38
39
  spec.add_development_dependency 'logger'
39
40
  spec.add_development_dependency 'rake'
40
41
  spec.add_development_dependency 'rspec'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: irb
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: logger
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -185,7 +199,7 @@ licenses:
185
199
  - MIT
186
200
  metadata:
187
201
  bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
188
- changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.0.0/CHANGELOG.md
202
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.1.0/CHANGELOG.md
189
203
  rubygems_mfa_required: 'true'
190
204
  rdoc_options: []
191
205
  require_paths: