jwt 2.8.2 → 2.10.2

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +189 -93
  4. data/lib/jwt/base64.rb +3 -0
  5. data/lib/jwt/claims/audience.rb +30 -0
  6. data/lib/jwt/claims/crit.rb +35 -0
  7. data/lib/jwt/claims/decode_verifier.rb +40 -0
  8. data/lib/jwt/claims/expiration.rb +32 -0
  9. data/lib/jwt/claims/issued_at.rb +22 -0
  10. data/lib/jwt/claims/issuer.rb +34 -0
  11. data/lib/jwt/claims/jwt_id.rb +35 -0
  12. data/lib/jwt/claims/not_before.rb +32 -0
  13. data/lib/jwt/claims/numeric.rb +77 -0
  14. data/lib/jwt/claims/required.rb +33 -0
  15. data/lib/jwt/claims/subject.rb +30 -0
  16. data/lib/jwt/claims/verification_methods.rb +20 -0
  17. data/lib/jwt/claims/verifier.rb +61 -0
  18. data/lib/jwt/claims.rb +74 -0
  19. data/lib/jwt/claims_validator.rb +6 -25
  20. data/lib/jwt/configuration/container.rb +20 -0
  21. data/lib/jwt/configuration/decode_configuration.rb +24 -0
  22. data/lib/jwt/configuration/jwk_configuration.rb +1 -0
  23. data/lib/jwt/configuration.rb +8 -0
  24. data/lib/jwt/decode.rb +28 -70
  25. data/lib/jwt/deprecations.rb +1 -0
  26. data/lib/jwt/encode.rb +17 -60
  27. data/lib/jwt/encoded_token.rb +139 -0
  28. data/lib/jwt/error.rb +34 -0
  29. data/lib/jwt/json.rb +1 -1
  30. data/lib/jwt/jwa/compat.rb +32 -0
  31. data/lib/jwt/jwa/ecdsa.rb +39 -25
  32. data/lib/jwt/jwa/eddsa.rb +20 -27
  33. data/lib/jwt/jwa/hmac.rb +25 -18
  34. data/lib/jwt/jwa/hmac_rbnacl.rb +43 -43
  35. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +40 -39
  36. data/lib/jwt/jwa/none.rb +8 -3
  37. data/lib/jwt/jwa/ps.rb +20 -15
  38. data/lib/jwt/jwa/rsa.rb +20 -10
  39. data/lib/jwt/jwa/signing_algorithm.rb +63 -0
  40. data/lib/jwt/jwa/unsupported.rb +9 -8
  41. data/lib/jwt/jwa/wrapper.rb +27 -9
  42. data/lib/jwt/jwa.rb +30 -34
  43. data/lib/jwt/jwk/ec.rb +2 -3
  44. data/lib/jwt/jwk/hmac.rb +2 -3
  45. data/lib/jwt/jwk/key_base.rb +1 -0
  46. data/lib/jwt/jwk/key_finder.rb +1 -0
  47. data/lib/jwt/jwk/kid_as_key_digest.rb +1 -0
  48. data/lib/jwt/jwk/okp_rbnacl.rb +3 -4
  49. data/lib/jwt/jwk/rsa.rb +2 -3
  50. data/lib/jwt/jwk/set.rb +2 -0
  51. data/lib/jwt/jwk.rb +1 -0
  52. data/lib/jwt/token.rb +112 -0
  53. data/lib/jwt/verify.rb +16 -93
  54. data/lib/jwt/version.rb +30 -9
  55. data/lib/jwt.rb +20 -0
  56. data/ruby-jwt.gemspec +1 -0
  57. metadata +36 -7
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # JWT
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt)
4
- [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=main)](https://github.com/jwt/ruby-jwt/actions)
4
+ [![Build Status](https://github.com/jwt/ruby-jwt/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/jwt/ruby-jwt/actions)
5
5
  [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
6
6
  [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
7
7
  [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
@@ -10,13 +10,30 @@ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools
10
10
 
11
11
  If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
12
12
 
13
- ## Announcements
14
- * Ruby 2.4 support was dropped in version 2.4.0
15
- * Ruby 1.9.3 support was dropped at December 31st, 2016.
16
- * 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)
17
-
18
13
  See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
19
14
 
15
+ ## Upcoming breaking changes
16
+
17
+ Notable changes in the upcoming **version 3.0**:
18
+
19
+ - The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed:
20
+ - Support for the non-standard SHA512256 algorithm will be removed.
21
+ - Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling.
22
+
23
+ - Base64 decoding will no longer fallback on the looser RFC 2045.
24
+
25
+ - Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new api](https://github.com/jwt/ruby-jwt/pull/626) and lead to the following deprecations:
26
+ - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`.
27
+ - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`.
28
+ - The `::JWT::JWA.create` method will be removed.
29
+ - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`.
30
+ - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
31
+ - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
32
+
33
+ - The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements:
34
+ - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed.
35
+ - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module.
36
+
20
37
  ## Sponsors
21
38
 
22
39
  |Logo|Message|
@@ -26,20 +43,32 @@ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
26
43
  ## Installing
27
44
 
28
45
  ### Using Rubygems:
46
+
29
47
  ```bash
30
48
  gem install jwt
31
49
  ```
32
50
 
33
51
  ### Using Bundler:
52
+
34
53
  Add the following to your Gemfile
35
54
  ```
36
55
  gem 'jwt'
37
56
  ```
57
+
38
58
  And run `bundle install`
39
59
 
60
+ Finally require the gem in your application
61
+ ```ruby
62
+ require 'jwt'
63
+ ```
64
+
40
65
  ## Algorithms and Usage
41
66
 
42
- The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
67
+ The jwt gem natively supports the NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms via the openssl library. The gem can be extended with additional or alternative implementations of the algorithms via extensions.
68
+
69
+ Additionally the EdDSA algorithm is supported via a [separate gem](https://rubygems.org/gems/jwt-eddsa).
70
+
71
+ For safe cryptographic signing, you need to specify the algorithm in the options hash whenever you call `JWT.decode` to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
43
72
 
44
73
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
45
74
 
@@ -65,18 +94,17 @@ The stricter base64 decoding when processing tokens can be done via the `strict_
65
94
  * none - unsigned token
66
95
 
67
96
  ```ruby
68
- require 'jwt'
69
97
 
70
98
  payload = { data: 'test' }
71
99
 
72
100
  # IMPORTANT: set nil as password parameter
73
- token = JWT.encode payload, nil, 'none'
101
+ token = JWT.encode(payload, nil, 'none')
74
102
 
75
103
  # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
76
104
  puts token
77
105
 
78
106
  # Set password to nil and validation to false otherwise this won't work
79
- decoded_token = JWT.decode token, nil, false
107
+ decoded_token = JWT.decode(token, nil, false)
80
108
 
81
109
  # Array
82
110
  # [
@@ -96,12 +124,12 @@ puts decoded_token
96
124
  # 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.
97
125
  hmac_secret = 'my$ecretK3y'
98
126
 
99
- token = JWT.encode payload, hmac_secret, 'HS256'
127
+ token = JWT.encode(payload, hmac_secret, 'HS256')
100
128
 
101
129
  # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
102
130
  puts token
103
131
 
104
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
132
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
105
133
 
106
134
  # Array
107
135
  # [
@@ -118,15 +146,15 @@ puts decoded_token
118
146
  * RS512 - RSA using SHA-512 hash algorithm
119
147
 
120
148
  ```ruby
121
- rsa_private = OpenSSL::PKey::RSA.generate 2048
149
+ rsa_private = OpenSSL::PKey::RSA.generate(2048)
122
150
  rsa_public = rsa_private.public_key
123
151
 
124
- token = JWT.encode payload, rsa_private, 'RS256'
152
+ token = JWT.encode(payload, rsa_private, 'RS256')
125
153
 
126
154
  # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
127
155
  puts token
128
156
 
129
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
157
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
130
158
 
131
159
  # Array
132
160
  # [
@@ -146,12 +174,12 @@ puts decoded_token
146
174
  ```ruby
147
175
  ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
148
176
 
149
- token = JWT.encode payload, ecdsa_key, 'ES256'
177
+ token = JWT.encode(payload, ecdsa_key, 'ES256')
150
178
 
151
179
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
152
180
  puts token
153
181
 
154
- decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
182
+ decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' })
155
183
 
156
184
  # Array
157
185
  # [
@@ -206,12 +234,12 @@ gem 'openssl', '~> 2.1'
206
234
  rsa_private = OpenSSL::PKey::RSA.generate 2048
207
235
  rsa_public = rsa_private.public_key
208
236
 
209
- token = JWT.encode payload, rsa_private, 'PS256'
237
+ token = JWT.encode(payload, rsa_private, 'PS256')
210
238
 
211
239
  # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
212
240
  puts token
213
241
 
214
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
242
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
215
243
 
216
244
  # Array
217
245
  # [
@@ -221,22 +249,57 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
221
249
  puts decoded_token
222
250
  ```
223
251
 
252
+ ### Add custom header fields
253
+ Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
254
+ To add custom header fields you need to pass `header_fields` parameter
255
+
256
+ ```ruby
257
+ token = JWT.encode(payload, key, algorithm='HS256', header_fields={})
258
+ ```
259
+
260
+ **Example:**
261
+
262
+ ```ruby
263
+
264
+ payload = { data: 'test' }
265
+
266
+ # IMPORTANT: set nil as password parameter
267
+ token = JWT.encode(payload, nil, 'none', { typ: 'JWT' })
268
+
269
+ # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
270
+ puts token
271
+
272
+ # Set password to nil and validation to false otherwise this won't work
273
+ decoded_token = JWT.decode(token, nil, false)
274
+
275
+ # Array
276
+ # [
277
+ # {"data"=>"test"}, # payload
278
+ # {"typ"=>"JWT", "alg"=>"none"} # header
279
+ # ]
280
+ puts decoded_token
281
+ ```
282
+
224
283
  ### **Custom algorithms**
225
284
 
226
- An object implementing custom signing or verification behaviour can be passed in the `algorithm` option when encoding and decoding. The given object needs to implement the method `valid_alg?` and `verify` and/or `alg` and `sign`, depending if object is used for encoding or decoding.
285
+ When encoding or decoding a token, you can pass in a custom object through the `algorithm` option to handle signing or verification. This custom object must include or extend the `JWT::JWA::SigningAlgorithm` module and implement certain methods:
286
+
287
+ - For decoding/verifying: The object must implement the methods `alg` and `verify`.
288
+ - For encoding/signing: The object must implement the methods `alg` and `sign`.
289
+
290
+ For customization options check the details from `JWT::JWA::SigningAlgorithm`.
291
+
227
292
 
228
293
  ```ruby
229
294
  module CustomHS512Algorithm
295
+ extend JWT::JWA::SigningAlgorithm
296
+
230
297
  def self.alg
231
298
  'HS512'
232
299
  end
233
300
 
234
- def self.valid_alg?(alg_to_validate)
235
- alg_to_validate == alg
236
- end
237
-
238
301
  def self.sign(data:, signing_key:)
239
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
302
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data)
240
303
  end
241
304
 
242
305
  def self.verify(data:, signature:, verification_key:)
@@ -248,49 +311,63 @@ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
248
311
  payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
249
312
  ```
250
313
 
251
- ## Support for reserved claim names
252
- JSON Web Token defines some reserved claim names and defines how they should be
253
- used. JWT supports these reserved claim names:
314
+ ## `JWT::Token` and `JWT::EncodedToken`
254
315
 
255
- - 'exp' (Expiration Time) Claim
256
- - 'nbf' (Not Before Time) Claim
257
- - 'iss' (Issuer) Claim
258
- - 'aud' (Audience) Claim
259
- - 'jti' (JWT ID) Claim
260
- - 'iat' (Issued At) Claim
261
- - 'sub' (Subject) Claim
316
+ The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
262
317
 
263
- ## Add custom header fields
264
- Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
265
- To add custom header fields you need to pass `header_fields` parameter
318
+ ```ruby
319
+ token = JWT::Token.new(payload: { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }, header: { kid: 'hmac' })
320
+ token.sign!(algorithm: 'HS256', key: "secret")
321
+
322
+ token.jwt # => "eyJhbGciOiJIUzI1N..."
323
+ ```
266
324
 
325
+ The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
267
326
  ```ruby
268
- token = JWT.encode payload, key, algorithm='HS256', header_fields={}
327
+ encoded_token = JWT::EncodedToken.new(token.jwt)
328
+
329
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
330
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "wrong_secret") # raises JWT::VerificationError
331
+ encoded_token.verify_claims!(:exp, :jti)
332
+ encoded_token.verify_claims!(sub: ["not-my-subject"]) # raises JWT::InvalidSubError
333
+ encoded_token.claim_errors(sub: ["not-my-subject"]).map(&:message) # => ["Invalid subject. Expected [\"not-my-subject\"], received my-subject"]
334
+ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
335
+ encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
269
336
  ```
270
337
 
271
- **Example:**
338
+ ### Detached payload
339
+
340
+ The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
272
341
 
273
342
  ```ruby
274
- require 'jwt'
343
+ token = JWT::Token.new(payload: { pay: 'load' })
344
+ token.sign!(algorithm: 'HS256', key: "secret")
345
+ token.detach_payload!
346
+ token.jwt # => "eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0"
347
+ token.encoded_payload # => "eyJwYXkiOiJsb2FkIn0"
348
+ ```
275
349
 
276
- payload = { data: 'test' }
350
+ The `JWT::EncodedToken` class can be used to decode a token with a detached payload by providing the payload to the token instance in separate.
277
351
 
278
- # IMPORTANT: set nil as password parameter
279
- token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
352
+ ```ruby
353
+ encoded_token = JWT::EncodedToken.new(token.jwt)
354
+ encoded_token.encoded_payload = "eyJwYXkiOiJsb2FkIn0"
355
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
356
+ encoded_token.payload # => {"pay"=>"load"}
357
+ ```
280
358
 
281
- # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
282
- puts token
359
+ ## Claims
283
360
 
284
- # Set password to nil and validation to false otherwise this won't work
285
- decoded_token = JWT.decode token, nil, false
361
+ JSON Web Token defines some reserved claim names and defines how they should be
362
+ used. JWT supports these reserved claim names:
286
363
 
287
- # Array
288
- # [
289
- # {"data"=>"test"}, # payload
290
- # {"typ"=>"JWT", "alg"=>"none"} # header
291
- # ]
292
- puts decoded_token
293
- ```
364
+ - 'exp' (Expiration Time) Claim
365
+ - 'nbf' (Not Before Time) Claim
366
+ - 'iss' (Issuer) Claim
367
+ - 'aud' (Audience) Claim
368
+ - 'jti' (JWT ID) Claim
369
+ - 'iat' (Issued At) Claim
370
+ - 'sub' (Subject) Claim
294
371
 
295
372
  ### Expiration Time Claim
296
373
 
@@ -304,10 +381,10 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
304
381
  exp = Time.now.to_i + 4 * 3600
305
382
  exp_payload = { data: 'data', exp: exp }
306
383
 
307
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
384
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
308
385
 
309
386
  begin
310
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
387
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
311
388
  rescue JWT::ExpiredSignature
312
389
  # Handle expired token, e.g. logout user or deny access
313
390
  end
@@ -316,7 +393,7 @@ end
316
393
  The Expiration Claim verification can be disabled.
317
394
  ```ruby
318
395
  # Decode token without raising JWT::ExpiredSignature error
319
- JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
396
+ JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' })
320
397
  ```
321
398
 
322
399
  **Adding Leeway**
@@ -328,11 +405,11 @@ leeway = 30 # seconds
328
405
  exp_payload = { data: 'data', exp: exp }
329
406
 
330
407
  # build expired token
331
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
408
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
332
409
 
333
410
  begin
334
411
  # add leeway to ensure the token is still accepted
335
- decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
412
+ decoded_token = JWT.decode(token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' })
336
413
  rescue JWT::ExpiredSignature
337
414
  # Handle expired token, e.g. logout user or deny access
338
415
  end
@@ -350,10 +427,10 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
350
427
  nbf = Time.now.to_i - 3600
351
428
  nbf_payload = { data: 'data', nbf: nbf }
352
429
 
353
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
430
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
354
431
 
355
432
  begin
356
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
433
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
357
434
  rescue JWT::ImmatureSignature
358
435
  # Handle invalid token, e.g. logout user or deny access
359
436
  end
@@ -362,7 +439,7 @@ end
362
439
  The Not Before Claim verification can be disabled.
363
440
  ```ruby
364
441
  # Decode token without raising JWT::ImmatureSignature error
365
- JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
442
+ JWT.decode(token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' })
366
443
  ```
367
444
 
368
445
  **Adding Leeway**
@@ -374,11 +451,11 @@ leeway = 30
374
451
  nbf_payload = { data: 'data', nbf: nbf }
375
452
 
376
453
  # build expired token
377
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
454
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
378
455
 
379
456
  begin
380
457
  # add leeway to ensure the token is valid
381
- decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
458
+ decoded_token = JWT.decode(token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' })
382
459
  rescue JWT::ImmatureSignature
383
460
  # Handle invalid token, e.g. logout user or deny access
384
461
  end
@@ -396,11 +473,11 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
396
473
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
397
474
  iss_payload = { data: 'data', iss: iss }
398
475
 
399
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
476
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
400
477
 
401
478
  begin
402
479
  # Add iss to the validation to check if the token has been manipulated
403
- decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
480
+ decoded_token = JWT.decode(token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' })
404
481
  rescue JWT::InvalidIssuerError
405
482
  # Handle invalid token, e.g. logout user or deny access
406
483
  end
@@ -411,24 +488,24 @@ On supported ruby versions (>= 2.5) you can also delegate to methods, on older v
411
488
  to convert them to proc (using `to_proc`)
412
489
 
413
490
  ```ruby
414
- JWT.decode token, hmac_secret, true,
491
+ JWT.decode(token, hmac_secret, true,
415
492
  iss: %r'https://my.awesome.website/',
416
493
  verify_iss: true,
417
- algorithm: 'HS256'
494
+ algorithm: 'HS256')
418
495
  ```
419
496
 
420
497
  ```ruby
421
- JWT.decode token, hmac_secret, true,
498
+ JWT.decode(token, hmac_secret, true,
422
499
  iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
423
500
  verify_iss: true,
424
- algorithm: 'HS256'
501
+ algorithm: 'HS256')
425
502
  ```
426
503
 
427
504
  ```ruby
428
- JWT.decode token, hmac_secret, true,
505
+ JWT.decode(token, hmac_secret, true,
429
506
  iss: method(:valid_issuer?),
430
507
  verify_iss: true,
431
- algorithm: 'HS256'
508
+ algorithm: 'HS256')
432
509
 
433
510
  # somewhere in the same class:
434
511
  def valid_issuer?(issuer)
@@ -446,11 +523,11 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
446
523
  aud = ['Young', 'Old']
447
524
  aud_payload = { data: 'data', aud: aud }
448
525
 
449
- token = JWT.encode aud_payload, hmac_secret, 'HS256'
526
+ token = JWT.encode(aud_payload, hmac_secret, 'HS256')
450
527
 
451
528
  begin
452
529
  # Add aud to the validation to check if the token has been manipulated
453
- decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
530
+ decoded_token = JWT.decode(token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' })
454
531
  rescue JWT::InvalidAudError
455
532
  # Handle invalid token, e.g. logout user or deny access
456
533
  puts 'Audience Error'
@@ -469,15 +546,15 @@ jti_raw = [hmac_secret, iat].join(':').to_s
469
546
  jti = Digest::MD5.hexdigest(jti_raw)
470
547
  jti_payload = { data: 'data', iat: iat, jti: jti }
471
548
 
472
- token = JWT.encode jti_payload, hmac_secret, 'HS256'
549
+ token = JWT.encode(jti_payload, hmac_secret, 'HS256')
473
550
 
474
551
  begin
475
552
  # If :verify_jti is true, validation will pass if a JTI is present
476
- #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
553
+ #decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' })
477
554
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
478
- decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
555
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' })
479
556
  # or
480
- decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
557
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' })
481
558
  rescue JWT::InvalidJtiError
482
559
  # Handle invalid token, e.g. logout user or deny access
483
560
  puts 'Error'
@@ -496,11 +573,11 @@ From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.or
496
573
  iat = Time.now.to_i
497
574
  iat_payload = { data: 'data', iat: iat }
498
575
 
499
- token = JWT.encode iat_payload, hmac_secret, 'HS256'
576
+ token = JWT.encode(iat_payload, hmac_secret, 'HS256')
500
577
 
501
578
  begin
502
579
  # Add iat to the validation to check if the token has been manipulated
503
- decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
580
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' })
504
581
  rescue JWT::InvalidIatError
505
582
  # Handle invalid token, e.g. logout user or deny access
506
583
  end
@@ -516,16 +593,32 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
516
593
  sub = 'Subject'
517
594
  sub_payload = { data: 'data', sub: sub }
518
595
 
519
- token = JWT.encode sub_payload, hmac_secret, 'HS256'
596
+ token = JWT.encode(sub_payload, hmac_secret, 'HS256')
520
597
 
521
598
  begin
522
599
  # Add sub to the validation to check if the token has been manipulated
523
- decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
600
+ decoded_token = JWT.decode(token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' })
524
601
  rescue JWT::InvalidSubError
525
602
  # Handle invalid token, e.g. logout user or deny access
526
603
  end
527
604
  ```
528
605
 
606
+ ### Standalone claim verification
607
+
608
+ The JWT claim verifications can be used to verify any Hash to include expected keys and values.
609
+
610
+ A few example on verifying the claims for a payload:
611
+ ```ruby
612
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :numeric, :exp)
613
+ JWT::Claims.valid_payload?({"exp" => Time.now.to_i + 10}, :exp)
614
+ # => true
615
+ JWT::Claims.payload_errors({"exp" => Time.now.to_i - 10}, :exp)
616
+ # => [#<struct JWT::Claims::Error message="Signature has expired">]
617
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
618
+
619
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject")
620
+ ```
621
+
529
622
  ### Finding a Key
530
623
 
531
624
  To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
@@ -536,7 +629,7 @@ iss_payload = { data: 'data', iss: issuers.first }
536
629
 
537
630
  secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
538
631
 
539
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
632
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
540
633
 
541
634
  begin
542
635
  # Add iss to the validation to check if the token has been manipulated
@@ -553,7 +646,7 @@ end
553
646
  You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
554
647
  ```ruby
555
648
  # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
556
- JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
649
+ JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' })
557
650
  ```
558
651
 
559
652
  ### X.509 certificates in x5c header
@@ -577,7 +670,7 @@ rescue JWT::DecodeError
577
670
  end
578
671
  ```
579
672
 
580
- ### JSON Web Key (JWK)
673
+ ## JSON Web Key (JWK)
581
674
 
582
675
  JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
583
676
 
@@ -604,7 +697,6 @@ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
604
697
  JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
605
698
  ```
606
699
 
607
-
608
700
  The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
609
701
  This can be used to implement caching of remotely fetched JWK Sets.
610
702
 
@@ -690,21 +782,25 @@ jwk_hash = jwk.export
690
782
  thumbprint_as_the_kid = jwk_hash[:kid]
691
783
  ```
692
784
 
693
- # Development and Tests
785
+ # Development and testing
694
786
 
695
- We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
787
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
696
788
 
697
789
  ```bash
698
- rake release
790
+ bundle install
791
+ bundle exec appraisal rake test
699
792
  ```
700
793
 
701
- The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
794
+ # Releasing
795
+
796
+ To cut a new release adjust the [version.rb](lib/jwt/version.rb) and [CHANGELOG](CHANGELOG.md) with desired version numbers and dates and commit the changes. Tag the release with the version number using the following command:
702
797
 
703
798
  ```bash
704
- bundle install
705
- bundle exec appraisal rake test
799
+ rake release:source_control_push
706
800
  ```
707
801
 
802
+ This will tag a new version an trigger a [GitHub action](.github/workflows/push_gem.yml) that eventually will push the gem to rubygems.org.
803
+
708
804
  ## How to contribute
709
805
  See [CONTRIBUTING](CONTRIBUTING.md).
710
806
 
data/lib/jwt/base64.rb CHANGED
@@ -4,15 +4,18 @@ require 'base64'
4
4
 
5
5
  module JWT
6
6
  # Base64 encoding and decoding
7
+ # @api private
7
8
  class Base64
8
9
  class << self
9
10
  # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded).
11
+ # @api private
10
12
  def url_encode(str)
11
13
  ::Base64.urlsafe_encode64(str, padding: false)
12
14
  end
13
15
 
14
16
  # Decode a string with URL-safe Base64 complying with RFC 4648.
15
17
  # Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software")
18
+ # @api private
16
19
  def url_decode(str)
17
20
  ::Base64.urlsafe_decode64(str)
18
21
  rescue ArgumentError => e
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Claims
5
+ # The Audience class is responsible for validating the audience claim ('aud') in a JWT token.
6
+ class Audience
7
+ # Initializes a new Audience instance.
8
+ #
9
+ # @param expected_audience [String, Array<String>] the expected audience(s) for the JWT token.
10
+ def initialize(expected_audience:)
11
+ @expected_audience = expected_audience
12
+ end
13
+
14
+ # Verifies the audience claim ('aud') in the JWT token.
15
+ #
16
+ # @param context [Object] the context containing the JWT payload.
17
+ # @param _args [Hash] additional arguments (not used).
18
+ # @raise [JWT::InvalidAudError] if the audience claim is invalid.
19
+ # @return [nil]
20
+ def verify!(context:, **_args)
21
+ aud = context.payload['aud']
22
+ raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :expected_audience
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Claims
5
+ # Responsible of validation the crit header
6
+ class Crit
7
+ # Initializes a new Crit instance.
8
+ #
9
+ # @param expected_crits [String] the expected crit header values for the JWT token.
10
+ def initialize(expected_crits:)
11
+ @expected_crits = Array(expected_crits)
12
+ end
13
+
14
+ # Verifies the critical claim ('crit') in the JWT token header.
15
+ #
16
+ # @param context [Object] the context containing the JWT payload and header.
17
+ # @param _args [Hash] additional arguments (not used).
18
+ # @raise [JWT::InvalidCritError] if the crit claim is invalid.
19
+ # @return [nil]
20
+ def verify!(context:, **_args)
21
+ raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit']
22
+ raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array)
23
+
24
+ missing = (expected_crits - context.header['crit'])
25
+ raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any?
26
+
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :expected_crits
33
+ end
34
+ end
35
+ end