jwt 2.7.1 → 2.10.3

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -27
  3. data/README.md +207 -101
  4. data/lib/jwt/base64.rb +19 -2
  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 +34 -3
  21. data/lib/jwt/configuration/decode_configuration.rb +24 -0
  22. data/lib/jwt/configuration/jwk_configuration.rb +2 -1
  23. data/lib/jwt/configuration.rb +8 -0
  24. data/lib/jwt/decode.rb +34 -82
  25. data/lib/jwt/deprecations.rb +49 -0
  26. data/lib/jwt/encode.rb +18 -67
  27. data/lib/jwt/encoded_token.rb +139 -0
  28. data/lib/jwt/error.rb +35 -0
  29. data/lib/jwt/json.rb +1 -1
  30. data/lib/jwt/jwa/compat.rb +32 -0
  31. data/lib/jwt/{algos → jwa}/ecdsa.rb +40 -26
  32. data/lib/jwt/jwa/eddsa.rb +35 -0
  33. data/lib/jwt/{algos → jwa}/hmac.rb +31 -20
  34. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  35. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
  36. data/lib/jwt/jwa/none.rb +24 -0
  37. data/lib/jwt/jwa/ps.rb +35 -0
  38. data/lib/jwt/jwa/rsa.rb +35 -0
  39. data/lib/jwt/jwa/signing_algorithm.rb +63 -0
  40. data/lib/jwt/jwa/unsupported.rb +20 -0
  41. data/lib/jwt/jwa/wrapper.rb +44 -0
  42. data/lib/jwt/jwa.rb +58 -0
  43. data/lib/jwt/jwk/ec.rb +44 -30
  44. data/lib/jwt/jwk/hmac.rb +2 -3
  45. data/lib/jwt/jwk/key_base.rb +4 -1
  46. data/lib/jwt/jwk/key_finder.rb +5 -4
  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 +3 -1
  51. data/lib/jwt/jwk.rb +2 -1
  52. data/lib/jwt/token.rb +112 -0
  53. data/lib/jwt/verify.rb +16 -89
  54. data/lib/jwt/version.rb +33 -11
  55. data/lib/jwt/x5c_key_finder.rb +2 -5
  56. data/lib/jwt.rb +24 -1
  57. data/ruby-jwt.gemspec +4 -0
  58. metadata +76 -18
  59. data/lib/jwt/algos/algo_wrapper.rb +0 -26
  60. data/lib/jwt/algos/eddsa.rb +0 -33
  61. data/lib/jwt/algos/hmac_rbnacl.rb +0 -53
  62. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +0 -52
  63. data/lib/jwt/algos/none.rb +0 -19
  64. data/lib/jwt/algos/ps.rb +0 -41
  65. data/lib/jwt/algos/rsa.rb +0 -23
  66. data/lib/jwt/algos/unsupported.rb +0 -19
  67. data/lib/jwt/algos.rb +0 -66
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,40 +43,68 @@ 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
 
75
+ ### Deprecation warnings
76
+
77
+ Deprecation warnings are logged once (`:once` option) by default to avoid spam in logs. Other options are `:silent` to completely silence warnings and `:warn` to log every time a deprecated path is executed.
78
+
79
+ ```ruby
80
+ JWT.configuration.deprecation_warnings = :warn # default is :once
81
+ ```
82
+
83
+ ### Base64 decoding
84
+
85
+ In the past the gem has been supporting the Base64 decoding specified in [RFC2045](https://www.rfc-editor.org/rfc/rfc2045) allowing newlines and blanks in the base64 encoded payload. In future versions base64 decoding will be stricter and only comply to [RFC4648](https://www.rfc-editor.org/rfc/rfc4648).
86
+
87
+ The stricter base64 decoding when processing tokens can be done via the `strict_base64_decoding` configuration accessor.
88
+ ```ruby
89
+ JWT.configuration.strict_base64_decoding = true # default is false
90
+ ```
91
+
46
92
  ### **NONE**
47
93
 
48
94
  * none - unsigned token
49
95
 
50
96
  ```ruby
51
- require 'jwt'
52
97
 
53
98
  payload = { data: 'test' }
54
99
 
55
100
  # IMPORTANT: set nil as password parameter
56
- token = JWT.encode payload, nil, 'none'
101
+ token = JWT.encode(payload, nil, 'none')
57
102
 
58
103
  # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
59
104
  puts token
60
105
 
61
106
  # Set password to nil and validation to false otherwise this won't work
62
- decoded_token = JWT.decode token, nil, false
107
+ decoded_token = JWT.decode(token, nil, false)
63
108
 
64
109
  # Array
65
110
  # [
@@ -72,7 +117,6 @@ puts decoded_token
72
117
  ### **HMAC**
73
118
 
74
119
  * HS256 - HMAC using SHA-256 hash algorithm
75
- * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
76
120
  * HS384 - HMAC using SHA-384 hash algorithm
77
121
  * HS512 - HMAC using SHA-512 hash algorithm
78
122
 
@@ -80,12 +124,12 @@ puts decoded_token
80
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.
81
125
  hmac_secret = 'my$ecretK3y'
82
126
 
83
- token = JWT.encode payload, hmac_secret, 'HS256'
127
+ token = JWT.encode(payload, hmac_secret, 'HS256')
84
128
 
85
129
  # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
86
130
  puts token
87
131
 
88
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
132
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
89
133
 
90
134
  # Array
91
135
  # [
@@ -95,12 +139,6 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
95
139
  puts decoded_token
96
140
  ```
97
141
 
98
- Note: If [RbNaCl](https://github.com/RubyCrypto/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl prior to 6.0.0 only support a maximum key size of 32 bytes for these algorithms.
99
-
100
- [RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
101
- [libsodium](https://github.com/jedisct1/libsodium), it can be installed
102
- on MacOS with `brew install libsodium`.
103
-
104
142
  ### **RSA**
105
143
 
106
144
  * RS256 - RSA using SHA-256 hash algorithm
@@ -108,15 +146,15 @@ on MacOS with `brew install libsodium`.
108
146
  * RS512 - RSA using SHA-512 hash algorithm
109
147
 
110
148
  ```ruby
111
- rsa_private = OpenSSL::PKey::RSA.generate 2048
149
+ rsa_private = OpenSSL::PKey::RSA.generate(2048)
112
150
  rsa_public = rsa_private.public_key
113
151
 
114
- token = JWT.encode payload, rsa_private, 'RS256'
152
+ token = JWT.encode(payload, rsa_private, 'RS256')
115
153
 
116
154
  # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
117
155
  puts token
118
156
 
119
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
157
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
120
158
 
121
159
  # Array
122
160
  # [
@@ -136,12 +174,12 @@ puts decoded_token
136
174
  ```ruby
137
175
  ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
138
176
 
139
- token = JWT.encode payload, ecdsa_key, 'ES256'
177
+ token = JWT.encode(payload, ecdsa_key, 'ES256')
140
178
 
141
179
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
142
180
  puts token
143
181
 
144
- decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
182
+ decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' })
145
183
 
146
184
  # Array
147
185
  # [
@@ -196,12 +234,12 @@ gem 'openssl', '~> 2.1'
196
234
  rsa_private = OpenSSL::PKey::RSA.generate 2048
197
235
  rsa_public = rsa_private.public_key
198
236
 
199
- token = JWT.encode payload, rsa_private, 'PS256'
237
+ token = JWT.encode(payload, rsa_private, 'PS256')
200
238
 
201
239
  # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
202
240
  puts token
203
241
 
204
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
242
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
205
243
 
206
244
  # Array
207
245
  # [
@@ -211,22 +249,57 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
211
249
  puts decoded_token
212
250
  ```
213
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
+
214
283
  ### **Custom algorithms**
215
284
 
216
- 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
+
217
292
 
218
293
  ```ruby
219
294
  module CustomHS512Algorithm
295
+ extend JWT::JWA::SigningAlgorithm
296
+
220
297
  def self.alg
221
298
  'HS512'
222
299
  end
223
300
 
224
- def self.valid_alg?(alg_to_validate)
225
- alg_to_validate == alg
226
- end
227
-
228
301
  def self.sign(data:, signing_key:)
229
- OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
302
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data)
230
303
  end
231
304
 
232
305
  def self.verify(data:, signature:, verification_key:)
@@ -238,49 +311,63 @@ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
238
311
  payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
239
312
  ```
240
313
 
241
- ## Support for reserved claim names
242
- JSON Web Token defines some reserved claim names and defines how they should be
243
- used. JWT supports these reserved claim names:
314
+ ## `JWT::Token` and `JWT::EncodedToken`
244
315
 
245
- - 'exp' (Expiration Time) Claim
246
- - 'nbf' (Not Before Time) Claim
247
- - 'iss' (Issuer) Claim
248
- - 'aud' (Audience) Claim
249
- - 'jti' (JWT ID) Claim
250
- - 'iat' (Issued At) Claim
251
- - 'sub' (Subject) Claim
316
+ The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
252
317
 
253
- ## Add custom header fields
254
- Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
255
- 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")
256
321
 
322
+ token.jwt # => "eyJhbGciOiJIUzI1N..."
323
+ ```
324
+
325
+ The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
257
326
  ```ruby
258
- 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'}
259
336
  ```
260
337
 
261
- **Example:**
338
+ ### Detached payload
339
+
340
+ The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
262
341
 
263
342
  ```ruby
264
- 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
+ ```
265
349
 
266
- 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.
267
351
 
268
- # IMPORTANT: set nil as password parameter
269
- 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
+ ```
270
358
 
271
- # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
272
- puts token
359
+ ## Claims
273
360
 
274
- # Set password to nil and validation to false otherwise this won't work
275
- 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:
276
363
 
277
- # Array
278
- # [
279
- # {"data"=>"test"}, # payload
280
- # {"typ"=>"JWT", "alg"=>"none"} # header
281
- # ]
282
- puts decoded_token
283
- ```
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
284
371
 
285
372
  ### Expiration Time Claim
286
373
 
@@ -294,10 +381,10 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
294
381
  exp = Time.now.to_i + 4 * 3600
295
382
  exp_payload = { data: 'data', exp: exp }
296
383
 
297
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
384
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
298
385
 
299
386
  begin
300
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
387
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
301
388
  rescue JWT::ExpiredSignature
302
389
  # Handle expired token, e.g. logout user or deny access
303
390
  end
@@ -306,7 +393,7 @@ end
306
393
  The Expiration Claim verification can be disabled.
307
394
  ```ruby
308
395
  # Decode token without raising JWT::ExpiredSignature error
309
- JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
396
+ JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' })
310
397
  ```
311
398
 
312
399
  **Adding Leeway**
@@ -318,11 +405,11 @@ leeway = 30 # seconds
318
405
  exp_payload = { data: 'data', exp: exp }
319
406
 
320
407
  # build expired token
321
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
408
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
322
409
 
323
410
  begin
324
411
  # add leeway to ensure the token is still accepted
325
- 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' })
326
413
  rescue JWT::ExpiredSignature
327
414
  # Handle expired token, e.g. logout user or deny access
328
415
  end
@@ -340,10 +427,10 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
340
427
  nbf = Time.now.to_i - 3600
341
428
  nbf_payload = { data: 'data', nbf: nbf }
342
429
 
343
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
430
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
344
431
 
345
432
  begin
346
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
433
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
347
434
  rescue JWT::ImmatureSignature
348
435
  # Handle invalid token, e.g. logout user or deny access
349
436
  end
@@ -352,7 +439,7 @@ end
352
439
  The Not Before Claim verification can be disabled.
353
440
  ```ruby
354
441
  # Decode token without raising JWT::ImmatureSignature error
355
- 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' })
356
443
  ```
357
444
 
358
445
  **Adding Leeway**
@@ -364,11 +451,11 @@ leeway = 30
364
451
  nbf_payload = { data: 'data', nbf: nbf }
365
452
 
366
453
  # build expired token
367
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
454
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
368
455
 
369
456
  begin
370
457
  # add leeway to ensure the token is valid
371
- 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' })
372
459
  rescue JWT::ImmatureSignature
373
460
  # Handle invalid token, e.g. logout user or deny access
374
461
  end
@@ -386,11 +473,11 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
386
473
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
387
474
  iss_payload = { data: 'data', iss: iss }
388
475
 
389
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
476
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
390
477
 
391
478
  begin
392
479
  # Add iss to the validation to check if the token has been manipulated
393
- 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' })
394
481
  rescue JWT::InvalidIssuerError
395
482
  # Handle invalid token, e.g. logout user or deny access
396
483
  end
@@ -401,24 +488,24 @@ On supported ruby versions (>= 2.5) you can also delegate to methods, on older v
401
488
  to convert them to proc (using `to_proc`)
402
489
 
403
490
  ```ruby
404
- JWT.decode token, hmac_secret, true,
491
+ JWT.decode(token, hmac_secret, true,
405
492
  iss: %r'https://my.awesome.website/',
406
493
  verify_iss: true,
407
- algorithm: 'HS256'
494
+ algorithm: 'HS256')
408
495
  ```
409
496
 
410
497
  ```ruby
411
- JWT.decode token, hmac_secret, true,
498
+ JWT.decode(token, hmac_secret, true,
412
499
  iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
413
500
  verify_iss: true,
414
- algorithm: 'HS256'
501
+ algorithm: 'HS256')
415
502
  ```
416
503
 
417
504
  ```ruby
418
- JWT.decode token, hmac_secret, true,
505
+ JWT.decode(token, hmac_secret, true,
419
506
  iss: method(:valid_issuer?),
420
507
  verify_iss: true,
421
- algorithm: 'HS256'
508
+ algorithm: 'HS256')
422
509
 
423
510
  # somewhere in the same class:
424
511
  def valid_issuer?(issuer)
@@ -436,11 +523,11 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
436
523
  aud = ['Young', 'Old']
437
524
  aud_payload = { data: 'data', aud: aud }
438
525
 
439
- token = JWT.encode aud_payload, hmac_secret, 'HS256'
526
+ token = JWT.encode(aud_payload, hmac_secret, 'HS256')
440
527
 
441
528
  begin
442
529
  # Add aud to the validation to check if the token has been manipulated
443
- 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' })
444
531
  rescue JWT::InvalidAudError
445
532
  # Handle invalid token, e.g. logout user or deny access
446
533
  puts 'Audience Error'
@@ -459,15 +546,15 @@ jti_raw = [hmac_secret, iat].join(':').to_s
459
546
  jti = Digest::MD5.hexdigest(jti_raw)
460
547
  jti_payload = { data: 'data', iat: iat, jti: jti }
461
548
 
462
- token = JWT.encode jti_payload, hmac_secret, 'HS256'
549
+ token = JWT.encode(jti_payload, hmac_secret, 'HS256')
463
550
 
464
551
  begin
465
552
  # If :verify_jti is true, validation will pass if a JTI is present
466
- #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' })
467
554
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
468
- 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' })
469
556
  # or
470
- 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' })
471
558
  rescue JWT::InvalidJtiError
472
559
  # Handle invalid token, e.g. logout user or deny access
473
560
  puts 'Error'
@@ -486,11 +573,11 @@ From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.or
486
573
  iat = Time.now.to_i
487
574
  iat_payload = { data: 'data', iat: iat }
488
575
 
489
- token = JWT.encode iat_payload, hmac_secret, 'HS256'
576
+ token = JWT.encode(iat_payload, hmac_secret, 'HS256')
490
577
 
491
578
  begin
492
579
  # Add iat to the validation to check if the token has been manipulated
493
- 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' })
494
581
  rescue JWT::InvalidIatError
495
582
  # Handle invalid token, e.g. logout user or deny access
496
583
  end
@@ -506,16 +593,32 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
506
593
  sub = 'Subject'
507
594
  sub_payload = { data: 'data', sub: sub }
508
595
 
509
- token = JWT.encode sub_payload, hmac_secret, 'HS256'
596
+ token = JWT.encode(sub_payload, hmac_secret, 'HS256')
510
597
 
511
598
  begin
512
599
  # Add sub to the validation to check if the token has been manipulated
513
- 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' })
514
601
  rescue JWT::InvalidSubError
515
602
  # Handle invalid token, e.g. logout user or deny access
516
603
  end
517
604
  ```
518
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
+
519
622
  ### Finding a Key
520
623
 
521
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.
@@ -526,7 +629,7 @@ iss_payload = { data: 'data', iss: issuers.first }
526
629
 
527
630
  secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
528
631
 
529
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
632
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
530
633
 
531
634
  begin
532
635
  # Add iss to the validation to check if the token has been manipulated
@@ -543,7 +646,7 @@ end
543
646
  You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
544
647
  ```ruby
545
648
  # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
546
- JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
649
+ JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' })
547
650
  ```
548
651
 
549
652
  ### X.509 certificates in x5c header
@@ -561,13 +664,13 @@ crls = crl_uris.map do |uri|
561
664
  end
562
665
 
563
666
  begin
564
- JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
667
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls } })
565
668
  rescue JWT::DecodeError
566
669
  # Handle error, e.g. x5c header certificate revoked or expired
567
670
  end
568
671
  ```
569
672
 
570
- ### JSON Web Key (JWK)
673
+ ## JSON Web Key (JWK)
571
674
 
572
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.
573
676
 
@@ -594,7 +697,6 @@ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
594
697
  JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
595
698
  ```
596
699
 
597
-
598
700
  The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
599
701
  This can be used to implement caching of remotely fetched JWK Sets.
600
702
 
@@ -680,21 +782,25 @@ jwk_hash = jwk.export
680
782
  thumbprint_as_the_kid = jwk_hash[:kid]
681
783
  ```
682
784
 
683
- # Development and Tests
785
+ # Development and testing
684
786
 
685
- 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.
686
788
 
687
789
  ```bash
688
- rake release
790
+ bundle install
791
+ bundle exec appraisal rake test
689
792
  ```
690
793
 
691
- 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:
692
797
 
693
798
  ```bash
694
- bundle install
695
- bundle exec appraisal rake test
799
+ rake release:source_control_push
696
800
  ```
697
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
+
698
804
  ## How to contribute
699
805
  See [CONTRIBUTING](CONTRIBUTING.md).
700
806
 
data/lib/jwt/base64.rb CHANGED
@@ -3,14 +3,31 @@
3
3
  require 'base64'
4
4
 
5
5
  module JWT
6
- # Base64 helpers
6
+ # Base64 encoding and decoding
7
+ # @api private
7
8
  class Base64
8
9
  class << self
10
+ # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded).
11
+ # @api private
9
12
  def url_encode(str)
10
- ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
13
+ ::Base64.urlsafe_encode64(str, padding: false)
11
14
  end
12
15
 
16
+ # Decode a string with URL-safe Base64 complying with RFC 4648.
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
13
19
  def url_decode(str)
20
+ ::Base64.urlsafe_decode64(str)
21
+ rescue ArgumentError => e
22
+ raise unless e.message == 'invalid base64'
23
+ raise Base64DecodeError, 'Invalid base64 encoding' if JWT.configuration.strict_base64_decoding
24
+
25
+ loose_urlsafe_decode64(str).tap do
26
+ Deprecations.warning('Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt', only_if_valid: true)
27
+ end
28
+ end
29
+
30
+ def loose_urlsafe_decode64(str)
14
31
  str += '=' * (4 - str.length.modulo(4))
15
32
  ::Base64.decode64(str.tr('-_', '+/'))
16
33
  end