jwt 1.5.6 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/AUTHORS +119 -0
  3. data/CHANGELOG.md +457 -8
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +399 -78
  7. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  8. data/lib/jwt/algos/ecdsa.rb +62 -0
  9. data/lib/jwt/algos/eddsa.rb +33 -0
  10. data/lib/jwt/algos/hmac.rb +73 -0
  11. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  12. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  13. data/lib/jwt/algos/none.rb +19 -0
  14. data/lib/jwt/algos/ps.rb +41 -0
  15. data/lib/jwt/algos/rsa.rb +21 -0
  16. data/lib/jwt/algos/unsupported.rb +19 -0
  17. data/lib/jwt/algos.rb +67 -0
  18. data/lib/jwt/base64.rb +19 -0
  19. data/lib/jwt/claims_validator.rb +37 -0
  20. data/lib/jwt/configuration/container.rb +21 -0
  21. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  22. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  23. data/lib/jwt/configuration.rb +15 -0
  24. data/lib/jwt/decode.rb +140 -29
  25. data/lib/jwt/encode.rb +79 -0
  26. data/lib/jwt/error.rb +8 -0
  27. data/lib/jwt/json.rb +10 -9
  28. data/lib/jwt/jwk/ec.rb +236 -0
  29. data/lib/jwt/jwk/hmac.rb +103 -0
  30. data/lib/jwt/jwk/key_base.rb +55 -0
  31. data/lib/jwt/jwk/key_finder.rb +46 -0
  32. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  33. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  34. data/lib/jwt/jwk/rsa.rb +203 -0
  35. data/lib/jwt/jwk/set.rb +80 -0
  36. data/lib/jwt/jwk/thumbprint.rb +26 -0
  37. data/lib/jwt/jwk.rb +55 -0
  38. data/lib/jwt/security_utils.rb +32 -0
  39. data/lib/jwt/verify.rb +58 -51
  40. data/lib/jwt/version.rb +24 -4
  41. data/lib/jwt/x5c_key_finder.rb +55 -0
  42. data/lib/jwt.rb +15 -176
  43. data/ruby-jwt.gemspec +19 -10
  44. metadata +51 -101
  45. data/.codeclimate.yml +0 -20
  46. data/.gitignore +0 -11
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -2
  49. data/.travis.yml +0 -13
  50. data/Gemfile +0 -4
  51. data/Manifest +0 -8
  52. data/Rakefile +0 -11
  53. data/spec/fixtures/certs/ec256-private.pem +0 -8
  54. data/spec/fixtures/certs/ec256-public.pem +0 -4
  55. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  56. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  57. data/spec/fixtures/certs/ec384-private.pem +0 -9
  58. data/spec/fixtures/certs/ec384-public.pem +0 -5
  59. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  60. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  61. data/spec/fixtures/certs/ec512-private.pem +0 -10
  62. data/spec/fixtures/certs/ec512-public.pem +0 -6
  63. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  64. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  65. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  66. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  67. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  68. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  69. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  70. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  71. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  72. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  73. data/spec/integration/readme_examples_spec.rb +0 -190
  74. data/spec/jwt/verify_spec.rb +0 -197
  75. data/spec/jwt_spec.rb +0 -240
  76. data/spec/spec_helper.rb +0 -31
data/README.md CHANGED
@@ -1,24 +1,33 @@
1
1
  # JWT
2
2
 
3
- [![Build Status](https://travis-ci.org/jwt/ruby-jwt.svg)](https://travis-ci.org/jwt/ruby-jwt)
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
5
  [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
5
6
  [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
6
7
  [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
7
8
 
8
- A pure ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
9
+ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
9
10
 
10
- If you have further questions releated to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
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).
11
12
 
12
13
  ## Announcements
13
-
14
- * Ruby 1.9.3 support will be dropped by December 31st, 2016.
14
+ * Ruby 2.4 support was dropped in version 2.4.0
15
+ * Ruby 1.9.3 support was dropped at December 31st, 2016.
15
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)
16
17
 
18
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
19
+
20
+ ## Sponsors
21
+
22
+ |Logo|Message|
23
+ |-|-|
24
+ |![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png)|If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth)|
25
+
17
26
  ## Installing
18
27
 
19
28
  ### Using Rubygems:
20
29
  ```bash
21
- sudo gem install jwt
30
+ gem install jwt
22
31
  ```
23
32
 
24
33
  ### Using Bundler:
@@ -30,23 +39,23 @@ And run `bundle install`
30
39
 
31
40
  ## Algorithms and Usage
32
41
 
33
- 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/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/).
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**
34
43
 
35
44
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
36
45
 
37
- **NONE**
46
+ ### **NONE**
38
47
 
39
48
  * none - unsigned token
40
49
 
41
50
  ```ruby
42
51
  require 'jwt'
43
52
 
44
- payload = {:data => 'test'}
53
+ payload = { data: 'test' }
45
54
 
46
55
  # IMPORTANT: set nil as password parameter
47
56
  token = JWT.encode payload, nil, 'none'
48
57
 
49
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
58
+ # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
50
59
  puts token
51
60
 
52
61
  # Set password to nil and validation to false otherwise this won't work
@@ -55,36 +64,44 @@ decoded_token = JWT.decode token, nil, false
55
64
  # Array
56
65
  # [
57
66
  # {"data"=>"test"}, # payload
58
- # {"typ"=>"JWT", "alg"=>"none"} # header
67
+ # {"alg"=>"none"} # header
59
68
  # ]
60
69
  puts decoded_token
61
70
  ```
62
71
 
63
- **HMAC** (default: HS256)
72
+ ### **HMAC**
64
73
 
65
- * HS256 - HMAC using SHA-256 hash algorithm (default)
74
+ * HS256 - HMAC using SHA-256 hash algorithm
75
+ * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
66
76
  * HS384 - HMAC using SHA-384 hash algorithm
67
77
  * HS512 - HMAC using SHA-512 hash algorithm
68
78
 
69
79
  ```ruby
80
+ # 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.
70
81
  hmac_secret = 'my$ecretK3y'
71
82
 
72
83
  token = JWT.encode payload, hmac_secret, 'HS256'
73
84
 
74
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY
85
+ # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
75
86
  puts token
76
87
 
77
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
88
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
78
89
 
79
90
  # Array
80
91
  # [
81
92
  # {"data"=>"test"}, # payload
82
- # {"typ"=>"JWT", "alg"=>"HS256"} # header
93
+ # {"alg"=>"HS256"} # header
83
94
  # ]
84
95
  puts decoded_token
85
96
  ```
86
97
 
87
- **RSA**
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
+ ### **RSA**
88
105
 
89
106
  * RS256 - RSA using SHA-256 hash algorithm
90
107
  * RS384 - RSA using SHA-384 hash algorithm
@@ -96,49 +113,130 @@ rsa_public = rsa_private.public_key
96
113
 
97
114
  token = JWT.encode payload, rsa_private, 'RS256'
98
115
 
99
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.c2FynXNyi6_PeKxrDGxfS3OLwQ8lTDbWBWdq7oMviCy2ZfFpzvW2E_odCWJrbLof-eplHCsKzW7MGAntHMALXgclm_Cs9i2Exi6BZHzpr9suYkrhIjwqV1tCgMBCQpdeMwIq6SyKVjgH3L51ivIt0-GDDPDH1Rcut3jRQzp3Q35bg3tcI2iVg7t3Msvl9QrxXAdYNFiS5KXH22aJZ8X_O2HgqVYBXfSB1ygTYUmKTIIyLbntPQ7R22rFko1knGWOgQCoYXwbtpuKRZVFrxX958L2gUWgb4jEQNf3fhOtkBm1mJpj-7BGst00o8g_3P2zHy-3aKgpPo1XlKQGjRrrxA
116
+ # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
100
117
  puts token
101
118
 
102
- decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
119
+ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
103
120
 
104
121
  # Array
105
122
  # [
106
123
  # {"data"=>"test"}, # payload
107
- # {"typ"=>"JWT", "alg"=>"RS256"} # header
124
+ # {"alg"=>"RS256"} # header
108
125
  # ]
109
126
  puts decoded_token
110
127
  ```
111
128
 
112
- **ECDSA**
129
+ ### **ECDSA**
113
130
 
114
131
  * ES256 - ECDSA using P-256 and SHA-256
115
132
  * ES384 - ECDSA using P-384 and SHA-384
116
133
  * ES512 - ECDSA using P-521 and SHA-512
134
+ * ES256K - ECDSA using P-256K and SHA-256
117
135
 
118
136
  ```ruby
119
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
120
- ecdsa_key.generate_key
121
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
122
- ecdsa_public.private_key = nil
137
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
123
138
 
124
139
  token = JWT.encode payload, ecdsa_key, 'ES256'
125
140
 
126
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.MEQCIAtShrxRwP1L9SapqaT4f7hajDJH4t_rfm-YlZcNDsBNAiB64M4-JRfyS8nRMlywtQ9lHbvvec9U54KznzOe1YxTyA
141
+ # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
127
142
  puts token
128
143
 
129
- decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' }
144
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
130
145
 
131
146
  # Array
132
147
  # [
133
148
  # {"test"=>"data"}, # payload
134
- # {"typ"=>"JWT", "alg"=>"ES256"} # header
149
+ # {"alg"=>"ES256"} # header
135
150
  # ]
136
151
  puts decoded_token
137
152
  ```
138
153
 
139
- **RSASSA-PSS**
154
+ ### **EDDSA**
155
+
156
+ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
157
+
158
+ ```ruby
159
+ gem 'rbnacl'
160
+ ```
140
161
 
141
- Not implemented.
162
+ For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
163
+
164
+ * ED25519
165
+
166
+ ```ruby
167
+ private_key = RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF')
168
+ public_key = private_key.verify_key
169
+ token = JWT.encode payload, private_key, 'ED25519'
170
+
171
+ # eyJhbGciOiJFRDI1NTE5In0.eyJkYXRhIjoidGVzdCJ9.6xIztXyOupskddGA_RvKU76V9b2dCQUYhoZEVFnRimJoPYIzZ2Fm47CWw8k2NTCNpgfAuxg9OXjaiVK7MvrbCQ
172
+ puts token
173
+
174
+ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
175
+ # Array
176
+ # [
177
+ # {"test"=>"data"}, # payload
178
+ # {"alg"=>"ED25519"} # header
179
+ # ]
180
+
181
+ ```
182
+
183
+ ### **RSASSA-PSS**
184
+
185
+ In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
186
+
187
+ ```ruby
188
+ gem 'openssl', '~> 2.1'
189
+ ```
190
+
191
+ * PS256 - RSASSA-PSS using SHA-256 hash algorithm
192
+ * PS384 - RSASSA-PSS using SHA-384 hash algorithm
193
+ * PS512 - RSASSA-PSS using SHA-512 hash algorithm
194
+
195
+ ```ruby
196
+ rsa_private = OpenSSL::PKey::RSA.generate 2048
197
+ rsa_public = rsa_private.public_key
198
+
199
+ token = JWT.encode payload, rsa_private, 'PS256'
200
+
201
+ # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
202
+ puts token
203
+
204
+ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
205
+
206
+ # Array
207
+ # [
208
+ # {"data"=>"test"}, # payload
209
+ # {"alg"=>"PS256"} # header
210
+ # ]
211
+ puts decoded_token
212
+ ```
213
+
214
+ ### **Custom algorithms**
215
+
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.
217
+
218
+ ```ruby
219
+ module CustomHS512Algorithm
220
+ def self.alg
221
+ 'HS512'
222
+ end
223
+
224
+ def self.valid_alg?(alg_to_validate)
225
+ alg_to_validate == alg
226
+ end
227
+
228
+ def self.sign(data:, signing_key:)
229
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
230
+ end
231
+
232
+ def self.verify(data:, signature:, verification_key:)
233
+ ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature)
234
+ end
235
+ end
236
+
237
+ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
238
+ payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
239
+ ```
142
240
 
143
241
  ## Support for reserved claim names
144
242
  JSON Web Token defines some reserved claim names and defines how they should be
@@ -152,6 +250,38 @@ used. JWT supports these reserved claim names:
152
250
  - 'iat' (Issued At) Claim
153
251
  - 'sub' (Subject) Claim
154
252
 
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
256
+
257
+ ```ruby
258
+ token = JWT.encode payload, key, algorithm='HS256', header_fields={}
259
+ ```
260
+
261
+ **Example:**
262
+
263
+ ```ruby
264
+ require 'jwt'
265
+
266
+ payload = { data: 'test' }
267
+
268
+ # IMPORTANT: set nil as password parameter
269
+ token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
270
+
271
+ # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
272
+ puts token
273
+
274
+ # Set password to nil and validation to false otherwise this won't work
275
+ decoded_token = JWT.decode token, nil, false
276
+
277
+ # Array
278
+ # [
279
+ # {"data"=>"test"}, # payload
280
+ # {"typ"=>"JWT", "alg"=>"none"} # header
281
+ # ]
282
+ puts decoded_token
283
+ ```
284
+
155
285
  ### Expiration Time Claim
156
286
 
157
287
  From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4):
@@ -162,31 +292,37 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
162
292
 
163
293
  ```ruby
164
294
  exp = Time.now.to_i + 4 * 3600
165
- exp_payload = { :data => 'data', :exp => exp }
295
+ exp_payload = { data: 'data', exp: exp }
166
296
 
167
297
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
168
298
 
169
299
  begin
170
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
300
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
171
301
  rescue JWT::ExpiredSignature
172
302
  # Handle expired token, e.g. logout user or deny access
173
303
  end
174
304
  ```
175
305
 
306
+ The Expiration Claim verification can be disabled.
307
+ ```ruby
308
+ # Decode token without raising JWT::ExpiredSignature error
309
+ JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
310
+ ```
311
+
176
312
  **Adding Leeway**
177
313
 
178
314
  ```ruby
179
315
  exp = Time.now.to_i - 10
180
316
  leeway = 30 # seconds
181
317
 
182
- exp_payload = { :data => 'data', :exp => exp }
318
+ exp_payload = { data: 'data', exp: exp }
183
319
 
184
320
  # build expired token
185
321
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
186
322
 
187
323
  begin
188
324
  # add leeway to ensure the token is still accepted
189
- decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' }
325
+ decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
190
326
  rescue JWT::ExpiredSignature
191
327
  # Handle expired token, e.g. logout user or deny access
192
328
  end
@@ -202,31 +338,37 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
202
338
 
203
339
  ```ruby
204
340
  nbf = Time.now.to_i - 3600
205
- nbf_payload = { :data => 'data', :nbf => nbf }
341
+ nbf_payload = { data: 'data', nbf: nbf }
206
342
 
207
343
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
208
344
 
209
345
  begin
210
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
346
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
211
347
  rescue JWT::ImmatureSignature
212
348
  # Handle invalid token, e.g. logout user or deny access
213
349
  end
214
350
  ```
215
351
 
352
+ The Not Before Claim verification can be disabled.
353
+ ```ruby
354
+ # Decode token without raising JWT::ImmatureSignature error
355
+ JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
356
+ ```
357
+
216
358
  **Adding Leeway**
217
359
 
218
360
  ```ruby
219
361
  nbf = Time.now.to_i + 10
220
362
  leeway = 30
221
363
 
222
- nbf_payload = { :data => 'data', :nbf => nbf }
364
+ nbf_payload = { data: 'data', nbf: nbf }
223
365
 
224
366
  # build expired token
225
367
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
226
368
 
227
369
  begin
228
370
  # add leeway to ensure the token is valid
229
- decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' }
371
+ decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
230
372
  rescue JWT::ImmatureSignature
231
373
  # Handle invalid token, e.g. logout user or deny access
232
374
  end
@@ -238,20 +380,52 @@ From [Oauth JSON Web Token 4.1.1. "iss" (Issuer) Claim](https://tools.ietf.org/h
238
380
 
239
381
  > The `iss` (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The `iss` value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
240
382
 
383
+ You can pass multiple allowed issuers as an Array, verification will pass if one of them matches the `iss` value in the payload.
384
+
241
385
  ```ruby
242
386
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
243
- iss_payload = { :data => 'data', :iss => iss }
387
+ iss_payload = { data: 'data', iss: iss }
244
388
 
245
389
  token = JWT.encode iss_payload, hmac_secret, 'HS256'
246
390
 
247
391
  begin
248
392
  # Add iss to the validation to check if the token has been manipulated
249
- decoded_token = JWT.decode token, hmac_secret, true, { :iss => iss, :verify_iss => true, :algorithm => 'HS256' }
393
+ decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
250
394
  rescue JWT::InvalidIssuerError
251
395
  # Handle invalid token, e.g. logout user or deny access
252
396
  end
253
397
  ```
254
398
 
399
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
400
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
401
+ to convert them to proc (using `to_proc`)
402
+
403
+ ```ruby
404
+ JWT.decode token, hmac_secret, true,
405
+ iss: %r'https://my.awesome.website/',
406
+ verify_iss: true,
407
+ algorithm: 'HS256'
408
+ ```
409
+
410
+ ```ruby
411
+ JWT.decode token, hmac_secret, true,
412
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
413
+ verify_iss: true,
414
+ algorithm: 'HS256'
415
+ ```
416
+
417
+ ```ruby
418
+ JWT.decode token, hmac_secret, true,
419
+ iss: method(:valid_issuer?),
420
+ verify_iss: true,
421
+ algorithm: 'HS256'
422
+
423
+ # somewhere in the same class:
424
+ def valid_issuer?(issuer)
425
+ # custom validation
426
+ end
427
+ ```
428
+
255
429
  ### Audience Claim
256
430
 
257
431
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -260,13 +434,13 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
260
434
 
261
435
  ```ruby
262
436
  aud = ['Young', 'Old']
263
- aud_payload = { :data => 'data', :aud => aud }
437
+ aud_payload = { data: 'data', aud: aud }
264
438
 
265
439
  token = JWT.encode aud_payload, hmac_secret, 'HS256'
266
440
 
267
441
  begin
268
442
  # Add aud to the validation to check if the token has been manipulated
269
- decoded_token = JWT.decode token, hmac_secret, true, { :aud => aud, :verify_aud => true, :algorithm => 'HS256' }
443
+ decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
270
444
  rescue JWT::InvalidAudError
271
445
  # Handle invalid token, e.g. logout user or deny access
272
446
  puts 'Audience Error'
@@ -283,37 +457,40 @@ From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/h
283
457
  # Use the secret and iat to create a unique key per request to prevent replay attacks
284
458
  jti_raw = [hmac_secret, iat].join(':').to_s
285
459
  jti = Digest::MD5.hexdigest(jti_raw)
286
- jti_payload = { :data => 'data', :iat => iat, :jti => jti }
460
+ jti_payload = { data: 'data', iat: iat, jti: jti }
287
461
 
288
462
  token = JWT.encode jti_payload, hmac_secret, 'HS256'
289
463
 
290
464
  begin
291
465
  # If :verify_jti is true, validation will pass if a JTI is present
292
- #decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => true, :algorithm => 'HS256' }
466
+ #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
293
467
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
294
- decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => proc { |jti| my_validation_method(jti) }, :algorithm => 'HS256' }
468
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
469
+ # or
470
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
295
471
  rescue JWT::InvalidJtiError
296
472
  # Handle invalid token, e.g. logout user or deny access
297
473
  puts 'Error'
298
474
  end
299
-
300
475
  ```
301
476
 
302
477
  ### Issued At Claim
303
478
 
304
479
  From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
305
480
 
306
- > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL.
481
+ > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. The `leeway` option is not taken into account when verifying this claim. The `iat_leeway` option was removed in version 2.2.0. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL.
482
+
483
+ **Handle Issued At Claim**
307
484
 
308
485
  ```ruby
309
486
  iat = Time.now.to_i
310
- iat_payload = { :data => 'data', :iat => iat }
487
+ iat_payload = { data: 'data', iat: iat }
311
488
 
312
489
  token = JWT.encode iat_payload, hmac_secret, 'HS256'
313
490
 
314
491
  begin
315
492
  # Add iat to the validation to check if the token has been manipulated
316
- decoded_token = JWT.decode token, hmac_secret, true, { :verify_iat => true, :algorithm => 'HS256' }
493
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
317
494
  rescue JWT::InvalidIatError
318
495
  # Handle invalid token, e.g. logout user or deny access
319
496
  end
@@ -327,18 +504,182 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
327
504
 
328
505
  ```ruby
329
506
  sub = 'Subject'
330
- sub_payload = { :data => 'data', :sub => sub }
507
+ sub_payload = { data: 'data', sub: sub }
331
508
 
332
509
  token = JWT.encode sub_payload, hmac_secret, 'HS256'
333
510
 
334
511
  begin
335
512
  # Add sub to the validation to check if the token has been manipulated
336
- decoded_token = JWT.decode token, hmac_secret, true, { 'sub' => sub, :verify_sub => true, :algorithm => 'HS256' }
513
+ decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
337
514
  rescue JWT::InvalidSubError
338
515
  # Handle invalid token, e.g. logout user or deny access
339
516
  end
340
517
  ```
341
518
 
519
+ ### Finding a Key
520
+
521
+ 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.
522
+
523
+ ```ruby
524
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
525
+ iss_payload = { data: 'data', iss: issuers.first }
526
+
527
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
528
+
529
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
530
+
531
+ begin
532
+ # Add iss to the validation to check if the token has been manipulated
533
+ decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
534
+ secrets[payload['iss']]
535
+ end
536
+ rescue JWT::InvalidIssuerError
537
+ # Handle invalid token, e.g. logout user or deny access
538
+ end
539
+ ```
540
+
541
+ ### Required Claims
542
+
543
+ You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
544
+ ```ruby
545
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
546
+ JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
547
+ ```
548
+
549
+ ### X.509 certificates in x5c header
550
+
551
+ A JWT signature can be verified using certificate(s) given in the `x5c` header. Before doing that, the trustworthiness of these certificate(s) must be established. This is done in accordance with RFC 5280 which (among other things) verifies the certificate(s) are issued by a trusted root certificate, the timestamps are valid, and none of the certificate(s) are revoked (i.e. being present in the root certificate's Certificate Revocation List).
552
+
553
+ ```ruby
554
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
555
+ crl_uris = root_certificates.map(&:crl_uris)
556
+ crls = crl_uris.map do |uri|
557
+ # look up cached CRL by `uri` and return it if found, otherwise continue
558
+ crl = Net::HTTP.get(uri)
559
+ crl = OpenSSL::X509::CRL.new(crl)
560
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
561
+ end
562
+
563
+ begin
564
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
565
+ rescue JWT::DecodeError
566
+ # Handle error, e.g. x5c header certificate revoked or expired
567
+ end
568
+ ```
569
+
570
+ ### JSON Web Key (JWK)
571
+
572
+ 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
+
574
+ To encode a JWT using your JWK:
575
+
576
+ ```ruby
577
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
578
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
579
+
580
+ # Encoding
581
+ payload = { data: 'data' }
582
+ token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
583
+
584
+ # JSON Web Key Set for advertising your signing keys
585
+ jwks_hash = JWT::JWK::Set.new(jwk).export
586
+ ```
587
+
588
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
589
+
590
+ ```ruby
591
+ jwks = JWT::JWK::Set.new(jwks_hash)
592
+ jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
593
+ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
594
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
595
+ ```
596
+
597
+
598
+ The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
599
+ This can be used to implement caching of remotely fetched JWK Sets.
600
+
601
+ 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`.
602
+ The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
603
+
604
+ Tokens without a specified `kid` are rejected by default.
605
+ This behaviour may be overwritten by setting the `allow_nil_jwks` option for `decode` to `true`.
606
+
607
+ ```ruby
608
+ jwks_loader = ->(options) do
609
+ # The jwk loader would fetch the set of JWKs from a trusted source.
610
+ # To avoid malicious requests triggering cache invalidations there needs to be
611
+ # some kind of grace time or other logic for determining the validity of the invalidation.
612
+ # This example only allows cache invalidations every 5 minutes.
613
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
614
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
615
+ @cached_keys = nil
616
+ end
617
+ @cached_keys ||= begin
618
+ @cache_last_update = Time.now.to_i
619
+ # Replace with your own JWKS fetching routine
620
+ jwks = JWT::JWK::Set.new(jwks_hash)
621
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
622
+ jwks
623
+ end
624
+ end
625
+
626
+ begin
627
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
628
+ rescue JWT::JWKError
629
+ # Handle problems with the provided JWKs
630
+ rescue JWT::DecodeError
631
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
632
+ end
633
+ ```
634
+
635
+ ### Importing and exporting JSON Web Keys
636
+
637
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
638
+ and export to either format with and without the private key included.
639
+
640
+ To include the private key in the export pass the `include_private` parameter to the export method.
641
+
642
+ ```ruby
643
+ # Import a JWK Hash (showing an HMAC example)
644
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
645
+
646
+ # Import an OpenSSL key
647
+ # You can optionally add descriptive parameters to the JWK
648
+ desc_params = { kid: 'my-kid', use: 'sig' }
649
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
650
+
651
+ # Export as JWK Hash (public key only by default)
652
+ jwk_hash = jwk.export
653
+ jwk_hash_with_private_key = jwk.export(include_private: true)
654
+
655
+ # Export as OpenSSL key
656
+ public_key = jwk.verify_key
657
+ private_key = jwk.signing_key if jwk.private?
658
+
659
+ # You can also import and export entire JSON Web Key Sets
660
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
661
+ jwks = JWT::JWK::Set.new(jwks_hash)
662
+ jwks_hash = jwks.export
663
+ ```
664
+
665
+ ### Key ID (kid) and JWKs
666
+
667
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
668
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
669
+ or can be given to the JWK instance on initialization.
670
+
671
+ ```ruby
672
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
673
+ # OR
674
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
675
+ # OR
676
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
677
+
678
+ jwk_hash = jwk.export
679
+
680
+ thumbprint_as_the_kid = jwk_hash[:kid]
681
+ ```
682
+
342
683
  # Development and Tests
343
684
 
344
685
  We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
@@ -347,40 +688,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
347
688
  rake release
348
689
  ```
349
690
 
350
- The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with
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.
351
692
 
352
693
  ```bash
353
- bundle exec rspec
694
+ bundle install
695
+ bundle exec appraisal rake test
354
696
  ```
355
697
 
356
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
698
+ ## How to contribute
699
+ See [CONTRIBUTING](CONTRIBUTING.md).
357
700
 
358
701
  ## Contributors
359
702
 
360
- * Jordan Brough <github.jordanb@xoxy.net>
361
- * Ilya Zhitomirskiy <ilya@joindiaspora.com>
362
- * Daniel Grippi <daniel@joindiaspora.com>
363
- * Jeff Lindsay <progrium@gmail.com>
364
- * Bob Aman <bob@sporkmonger.com>
365
- * Micah Gates <github@mgates.com>
366
- * Rob Wygand <rob@wygand.com>
367
- * Ariel Salomon (Oscil8)
368
- * Paul Battley <pbattley@gmail.com>
369
- * Zane Shannon [@zshannon](https://github.com/zshannon)
370
- * Brian Fletcher [@punkle](https://github.com/punkle)
371
- * Alex [@ZhangHanDong](https://github.com/ZhangHanDong)
372
- * John Downey [@jtdowney](https://github.com/jtdowney)
373
- * Adam Greene [@skippy](https://github.com/skippy)
374
- * Tim Rudat [@excpt](https://github.com/excpt) <timrudat@gmail.com> - Maintainer
703
+ See [AUTHORS](AUTHORS).
375
704
 
376
705
  ## License
377
706
 
378
- MIT
379
-
380
- Copyright (c) 2011 Jeff Lindsay
381
-
382
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
383
-
384
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
385
-
386
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
707
+ See [LICENSE](LICENSE).