jwt 2.1.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +6 -18
  3. data/.github/workflows/coverage.yml +27 -0
  4. data/.github/workflows/test.yml +67 -0
  5. data/.gitignore +3 -1
  6. data/.reek.yml +21 -39
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +21 -52
  9. data/{.ebert.yml → .sourcelevel.yml} +3 -4
  10. data/AUTHORS +119 -0
  11. data/Appraisals +13 -0
  12. data/CHANGELOG.md +329 -19
  13. data/CODE_OF_CONDUCT.md +84 -0
  14. data/CONTRIBUTING.md +99 -0
  15. data/Gemfile +4 -0
  16. data/README.md +261 -100
  17. data/Rakefile +6 -1
  18. data/lib/jwt/algos/ecdsa.rb +37 -8
  19. data/lib/jwt/algos/eddsa.rb +16 -4
  20. data/lib/jwt/algos/hmac.rb +3 -0
  21. data/lib/jwt/algos/none.rb +17 -0
  22. data/lib/jwt/algos/ps.rb +43 -0
  23. data/lib/jwt/algos/rsa.rb +4 -1
  24. data/lib/jwt/algos/unsupported.rb +7 -4
  25. data/lib/jwt/algos.rb +44 -0
  26. data/lib/jwt/base64.rb +19 -0
  27. data/lib/jwt/claims_validator.rb +37 -0
  28. data/lib/jwt/configuration/container.rb +21 -0
  29. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  30. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  31. data/lib/jwt/configuration.rb +15 -0
  32. data/lib/jwt/decode.rb +120 -24
  33. data/lib/jwt/encode.rb +43 -25
  34. data/lib/jwt/error.rb +6 -0
  35. data/lib/jwt/json.rb +18 -0
  36. data/lib/jwt/jwk/ec.rb +199 -0
  37. data/lib/jwt/jwk/hmac.rb +67 -0
  38. data/lib/jwt/jwk/key_base.rb +35 -0
  39. data/lib/jwt/jwk/key_finder.rb +62 -0
  40. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  41. data/lib/jwt/jwk/rsa.rb +138 -0
  42. data/lib/jwt/jwk/thumbprint.rb +26 -0
  43. data/lib/jwt/jwk.rb +52 -0
  44. data/lib/jwt/security_utils.rb +8 -0
  45. data/lib/jwt/signature.rb +7 -22
  46. data/lib/jwt/verify.rb +19 -8
  47. data/lib/jwt/version.rb +6 -2
  48. data/lib/jwt/x5c_key_finder.rb +55 -0
  49. data/lib/jwt.rb +12 -44
  50. data/ruby-jwt.gemspec +13 -9
  51. metadata +44 -97
  52. data/.travis.yml +0 -14
  53. data/Manifest +0 -8
  54. data/lib/jwt/default_options.rb +0 -15
  55. data/spec/fixtures/certs/ec256-private.pem +0 -8
  56. data/spec/fixtures/certs/ec256-public.pem +0 -4
  57. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  58. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  59. data/spec/fixtures/certs/ec384-private.pem +0 -9
  60. data/spec/fixtures/certs/ec384-public.pem +0 -5
  61. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  62. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  63. data/spec/fixtures/certs/ec512-private.pem +0 -10
  64. data/spec/fixtures/certs/ec512-public.pem +0 -6
  65. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  66. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  67. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  68. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  69. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  70. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  71. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  72. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  73. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  74. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  75. data/spec/integration/readme_examples_spec.rb +0 -202
  76. data/spec/jwt/verify_spec.rb +0 -232
  77. data/spec/jwt_spec.rb +0 -315
  78. data/spec/spec_helper.rb +0 -28
data/README.md CHANGED
@@ -1,25 +1,34 @@
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://travis-ci.org/jwt/ruby-jwt.svg)](https://travis-ci.org/jwt/ruby-jwt)
4
+ [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](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)
8
+ [![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
8
9
 
9
- A pure ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
10
+ A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
10
11
 
11
12
  If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
12
13
 
13
14
  ## Announcements
14
-
15
+ * Ruby 2.4 support was dropped in version 2.4.0
15
16
  * Ruby 1.9.3 support was dropped at December 31st, 2016.
16
17
  * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
17
18
 
19
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
20
+
21
+ ## Sponsors
22
+
23
+ |Logo|Message|
24
+ |-|-|
25
+ |![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)|
26
+
18
27
  ## Installing
19
28
 
20
29
  ### Using Rubygems:
21
30
  ```bash
22
- sudo gem install jwt
31
+ gem install jwt
23
32
  ```
24
33
 
25
34
  ### Using Bundler:
@@ -31,23 +40,23 @@ And run `bundle install`
31
40
 
32
41
  ## Algorithms and Usage
33
42
 
34
- 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/).
43
+ 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**
35
44
 
36
45
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
37
46
 
38
- **NONE**
47
+ ### **NONE**
39
48
 
40
49
  * none - unsigned token
41
50
 
42
51
  ```ruby
43
52
  require 'jwt'
44
53
 
45
- payload = {:data => 'test'}
54
+ payload = { data: 'test' }
46
55
 
47
56
  # IMPORTANT: set nil as password parameter
48
57
  token = JWT.encode payload, nil, 'none'
49
58
 
50
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
59
+ # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
51
60
  puts token
52
61
 
53
62
  # Set password to nil and validation to false otherwise this won't work
@@ -61,7 +70,7 @@ decoded_token = JWT.decode token, nil, false
61
70
  puts decoded_token
62
71
  ```
63
72
 
64
- **HMAC**
73
+ ### **HMAC**
65
74
 
66
75
  * HS256 - HMAC using SHA-256 hash algorithm
67
76
  * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
@@ -69,14 +78,15 @@ puts decoded_token
69
78
  * HS512 - HMAC using SHA-512 hash algorithm
70
79
 
71
80
  ```ruby
81
+ # The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
72
82
  hmac_secret = 'my$ecretK3y'
73
83
 
74
84
  token = JWT.encode payload, hmac_secret, 'HS256'
75
85
 
76
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY
86
+ # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
77
87
  puts token
78
88
 
79
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
89
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
80
90
 
81
91
  # Array
82
92
  # [
@@ -92,7 +102,7 @@ Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt
92
102
  [libsodium](https://github.com/jedisct1/libsodium), it can be installed
93
103
  on MacOS with `brew install libsodium`.
94
104
 
95
- **RSA**
105
+ ### **RSA**
96
106
 
97
107
  * RS256 - RSA using SHA-256 hash algorithm
98
108
  * RS384 - RSA using SHA-384 hash algorithm
@@ -104,10 +114,10 @@ rsa_public = rsa_private.public_key
104
114
 
105
115
  token = JWT.encode payload, rsa_private, 'RS256'
106
116
 
107
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.c2FynXNyi6_PeKxrDGxfS3OLwQ8lTDbWBWdq7oMviCy2ZfFpzvW2E_odCWJrbLof-eplHCsKzW7MGAntHMALXgclm_Cs9i2Exi6BZHzpr9suYkrhIjwqV1tCgMBCQpdeMwIq6SyKVjgH3L51ivIt0-GDDPDH1Rcut3jRQzp3Q35bg3tcI2iVg7t3Msvl9QrxXAdYNFiS5KXH22aJZ8X_O2HgqVYBXfSB1ygTYUmKTIIyLbntPQ7R22rFko1knGWOgQCoYXwbtpuKRZVFrxX958L2gUWgb4jEQNf3fhOtkBm1mJpj-7BGst00o8g_3P2zHy-3aKgpPo1XlKQGjRrrxA
117
+ # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
108
118
  puts token
109
119
 
110
- decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
120
+ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
111
121
 
112
122
  # Array
113
123
  # [
@@ -117,24 +127,22 @@ decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
117
127
  puts decoded_token
118
128
  ```
119
129
 
120
- **ECDSA**
130
+ ### **ECDSA**
121
131
 
122
132
  * ES256 - ECDSA using P-256 and SHA-256
123
133
  * ES384 - ECDSA using P-384 and SHA-384
124
134
  * ES512 - ECDSA using P-521 and SHA-512
135
+ * ES256K - ECDSA using P-256K and SHA-256
125
136
 
126
137
  ```ruby
127
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
128
- ecdsa_key.generate_key
129
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
130
- ecdsa_public.private_key = nil
138
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
131
139
 
132
140
  token = JWT.encode payload, ecdsa_key, 'ES256'
133
141
 
134
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.MEQCIAtShrxRwP1L9SapqaT4f7hajDJH4t_rfm-YlZcNDsBNAiB64M4-JRfyS8nRMlywtQ9lHbvvec9U54KznzOe1YxTyA
142
+ # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
135
143
  puts token
136
144
 
137
- decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' }
145
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
138
146
 
139
147
  # Array
140
148
  # [
@@ -144,7 +152,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' }
144
152
  puts decoded_token
145
153
  ```
146
154
 
147
- **EDDSA**
155
+ ### **EDDSA**
148
156
 
149
157
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
150
158
 
@@ -154,17 +162,17 @@ gem 'rbnacl'
154
162
 
155
163
  For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
156
164
 
157
- * ED25519
165
+ * ED25519
158
166
 
159
- ```ruby
160
- private_key = RbNaCl::Signatures::Ed25519::SigningKey.new("abcdefghijklmnopqrstuvwxyzABCDEF")
167
+ ```ruby
168
+ private_key = RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF')
161
169
  public_key = private_key.verify_key
162
- token = JWT.encode payload, private_key, 'ED25519'
170
+ token = JWT.encode payload, private_key, 'ED25519'
163
171
 
164
- # eyJhbGciOiJFRDI1NTE5In0.eyJ0ZXN0IjoiZGF0YSJ9.-Ki0vxVOlsPXovPsYRT_9OXrLSgQd4RDAgCLY_PLmcP4q32RYy-yUUmX82ycegdekR9wo26me1wOzjmSU5nTCQ
172
+ # eyJhbGciOiJFRDI1NTE5In0.eyJkYXRhIjoidGVzdCJ9.6xIztXyOupskddGA_RvKU76V9b2dCQUYhoZEVFnRimJoPYIzZ2Fm47CWw8k2NTCNpgfAuxg9OXjaiVK7MvrbCQ
165
173
  puts token
166
174
 
167
- decoded_token = JWT.decode token, public_key, true, {:algorithm => 'ED25519' }
175
+ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
168
176
  # Array
169
177
  # [
170
178
  # {"test"=>"data"}, # payload
@@ -173,9 +181,36 @@ decoded_token = JWT.decode token, public_key, true, {:algorithm => 'ED25519' }
173
181
 
174
182
  ```
175
183
 
176
- **RSASSA-PSS**
184
+ ### **RSASSA-PSS**
185
+
186
+ In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
187
+
188
+ ```ruby
189
+ gem 'openssl', '~> 2.1'
190
+ ```
191
+
192
+ * PS256 - RSASSA-PSS using SHA-256 hash algorithm
193
+ * PS384 - RSASSA-PSS using SHA-384 hash algorithm
194
+ * PS512 - RSASSA-PSS using SHA-512 hash algorithm
195
+
196
+ ```ruby
197
+ rsa_private = OpenSSL::PKey::RSA.generate 2048
198
+ rsa_public = rsa_private.public_key
199
+
200
+ token = JWT.encode payload, rsa_private, 'PS256'
201
+
202
+ # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
203
+ puts token
204
+
205
+ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
177
206
 
178
- Not implemented.
207
+ # Array
208
+ # [
209
+ # {"data"=>"test"}, # payload
210
+ # {"alg"=>"PS256"} # header
211
+ # ]
212
+ puts decoded_token
213
+ ```
179
214
 
180
215
  ## Support for reserved claim names
181
216
  JSON Web Token defines some reserved claim names and defines how they should be
@@ -190,7 +225,7 @@ used. JWT supports these reserved claim names:
190
225
  - 'sub' (Subject) Claim
191
226
 
192
227
  ## Add custom header fields
193
- Ruby-jwt gem supports custom [header fields] (https://tools.ietf.org/html/rfc7519#section-5)
228
+ Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
194
229
  To add custom header fields you need to pass `header_fields` parameter
195
230
 
196
231
  ```ruby
@@ -202,12 +237,12 @@ token = JWT.encode payload, key, algorithm='HS256', header_fields={}
202
237
  ```ruby
203
238
  require 'jwt'
204
239
 
205
- payload = {:data => 'test'}
240
+ payload = { data: 'test' }
206
241
 
207
242
  # IMPORTANT: set nil as password parameter
208
- token = JWT.encode payload, nil, 'none', { :typ => "JWT" }
243
+ token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
209
244
 
210
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
245
+ # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
211
246
  puts token
212
247
 
213
248
  # Set password to nil and validation to false otherwise this won't work
@@ -231,31 +266,37 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
231
266
 
232
267
  ```ruby
233
268
  exp = Time.now.to_i + 4 * 3600
234
- exp_payload = { :data => 'data', :exp => exp }
269
+ exp_payload = { data: 'data', exp: exp }
235
270
 
236
271
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
237
272
 
238
273
  begin
239
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
274
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
240
275
  rescue JWT::ExpiredSignature
241
276
  # Handle expired token, e.g. logout user or deny access
242
277
  end
243
278
  ```
244
279
 
280
+ The Expiration Claim verification can be disabled.
281
+ ```ruby
282
+ # Decode token without raising JWT::ExpiredSignature error
283
+ JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
284
+ ```
285
+
245
286
  **Adding Leeway**
246
287
 
247
288
  ```ruby
248
289
  exp = Time.now.to_i - 10
249
290
  leeway = 30 # seconds
250
291
 
251
- exp_payload = { :data => 'data', :exp => exp }
292
+ exp_payload = { data: 'data', exp: exp }
252
293
 
253
294
  # build expired token
254
295
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
255
296
 
256
297
  begin
257
298
  # add leeway to ensure the token is still accepted
258
- decoded_token = JWT.decode token, hmac_secret, true, { :exp_leeway => leeway, :algorithm => 'HS256' }
299
+ decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
259
300
  rescue JWT::ExpiredSignature
260
301
  # Handle expired token, e.g. logout user or deny access
261
302
  end
@@ -271,31 +312,37 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
271
312
 
272
313
  ```ruby
273
314
  nbf = Time.now.to_i - 3600
274
- nbf_payload = { :data => 'data', :nbf => nbf }
315
+ nbf_payload = { data: 'data', nbf: nbf }
275
316
 
276
317
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
277
318
 
278
319
  begin
279
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
320
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
280
321
  rescue JWT::ImmatureSignature
281
322
  # Handle invalid token, e.g. logout user or deny access
282
323
  end
283
324
  ```
284
325
 
326
+ The Not Before Claim verification can be disabled.
327
+ ```ruby
328
+ # Decode token without raising JWT::ImmatureSignature error
329
+ JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
330
+ ```
331
+
285
332
  **Adding Leeway**
286
333
 
287
334
  ```ruby
288
335
  nbf = Time.now.to_i + 10
289
336
  leeway = 30
290
337
 
291
- nbf_payload = { :data => 'data', :nbf => nbf }
338
+ nbf_payload = { data: 'data', nbf: nbf }
292
339
 
293
340
  # build expired token
294
341
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
295
342
 
296
343
  begin
297
344
  # add leeway to ensure the token is valid
298
- decoded_token = JWT.decode token, hmac_secret, true, { :nbf_leeway => leeway, :algorithm => 'HS256' }
345
+ decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
299
346
  rescue JWT::ImmatureSignature
300
347
  # Handle invalid token, e.g. logout user or deny access
301
348
  end
@@ -311,18 +358,48 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
311
358
 
312
359
  ```ruby
313
360
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
314
- iss_payload = { :data => 'data', :iss => iss }
361
+ iss_payload = { data: 'data', iss: iss }
315
362
 
316
363
  token = JWT.encode iss_payload, hmac_secret, 'HS256'
317
364
 
318
365
  begin
319
366
  # Add iss to the validation to check if the token has been manipulated
320
- decoded_token = JWT.decode token, hmac_secret, true, { :iss => iss, :verify_iss => true, :algorithm => 'HS256' }
367
+ decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
321
368
  rescue JWT::InvalidIssuerError
322
369
  # Handle invalid token, e.g. logout user or deny access
323
370
  end
324
371
  ```
325
372
 
373
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
374
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
375
+ to convert them to proc (using `to_proc`)
376
+
377
+ ```ruby
378
+ JWT.decode token, hmac_secret, true,
379
+ iss: %r'https://my.awesome.website/',
380
+ verify_iss: true,
381
+ algorithm: 'HS256'
382
+ ```
383
+
384
+ ```ruby
385
+ JWT.decode token, hmac_secret, true,
386
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
387
+ verify_iss: true,
388
+ algorithm: 'HS256'
389
+ ```
390
+
391
+ ```ruby
392
+ JWT.decode token, hmac_secret, true,
393
+ iss: method(:valid_issuer?),
394
+ verify_iss: true,
395
+ algorithm: 'HS256'
396
+
397
+ # somewhere in the same class:
398
+ def valid_issuer?(issuer)
399
+ # custom validation
400
+ end
401
+ ```
402
+
326
403
  ### Audience Claim
327
404
 
328
405
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -331,13 +408,13 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
331
408
 
332
409
  ```ruby
333
410
  aud = ['Young', 'Old']
334
- aud_payload = { :data => 'data', :aud => aud }
411
+ aud_payload = { data: 'data', aud: aud }
335
412
 
336
413
  token = JWT.encode aud_payload, hmac_secret, 'HS256'
337
414
 
338
415
  begin
339
416
  # Add aud to the validation to check if the token has been manipulated
340
- decoded_token = JWT.decode token, hmac_secret, true, { :aud => aud, :verify_aud => true, :algorithm => 'HS256' }
417
+ decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
341
418
  rescue JWT::InvalidAudError
342
419
  # Handle invalid token, e.g. logout user or deny access
343
420
  puts 'Audience Error'
@@ -354,83 +431,187 @@ From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/h
354
431
  # Use the secret and iat to create a unique key per request to prevent replay attacks
355
432
  jti_raw = [hmac_secret, iat].join(':').to_s
356
433
  jti = Digest::MD5.hexdigest(jti_raw)
357
- jti_payload = { :data => 'data', :iat => iat, :jti => jti }
434
+ jti_payload = { data: 'data', iat: iat, jti: jti }
358
435
 
359
436
  token = JWT.encode jti_payload, hmac_secret, 'HS256'
360
437
 
361
438
  begin
362
439
  # If :verify_jti is true, validation will pass if a JTI is present
363
- #decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => true, :algorithm => 'HS256' }
440
+ #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
364
441
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
365
- decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => proc { |jti| my_validation_method(jti) }, :algorithm => 'HS256' }
442
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
443
+ # or
444
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
366
445
  rescue JWT::InvalidJtiError
367
446
  # Handle invalid token, e.g. logout user or deny access
368
447
  puts 'Error'
369
448
  end
370
-
371
449
  ```
372
450
 
373
451
  ### Issued At Claim
374
452
 
375
453
  From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
376
454
 
377
- > 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.
455
+ > 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.
378
456
 
379
457
  **Handle Issued At Claim**
380
458
 
381
459
  ```ruby
382
460
  iat = Time.now.to_i
383
- iat_payload = { :data => 'data', :iat => iat }
461
+ iat_payload = { data: 'data', iat: iat }
384
462
 
385
463
  token = JWT.encode iat_payload, hmac_secret, 'HS256'
386
464
 
387
465
  begin
388
466
  # Add iat to the validation to check if the token has been manipulated
389
- decoded_token = JWT.decode token, hmac_secret, true, { :verify_iat => true, :algorithm => 'HS256' }
467
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
390
468
  rescue JWT::InvalidIatError
391
469
  # Handle invalid token, e.g. logout user or deny access
392
470
  end
393
471
  ```
394
472
 
395
- **Adding Leeway**
473
+ ### Subject Claim
396
474
 
397
- ```ruby
398
- iat = Time.now.to_i + 10
399
- leeway = 30 # seconds
475
+ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2):
400
476
 
401
- iat_payload = { :data => 'data', :iat => iat }
477
+ > The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
402
478
 
403
- # build token issued in the future
404
- token = JWT.encode iat_payload, hmac_secret, 'HS256'
479
+ ```ruby
480
+ sub = 'Subject'
481
+ sub_payload = { data: 'data', sub: sub }
482
+
483
+ token = JWT.encode sub_payload, hmac_secret, 'HS256'
405
484
 
406
485
  begin
407
- # add leeway to ensure the token is accepted
408
- decoded_token = JWT.decode token, hmac_secret, true, { :iat_leeway => leeway, :verify_iat => true, :algorithm => 'HS256' }
409
- rescue JWT::InvalidIatError
486
+ # Add sub to the validation to check if the token has been manipulated
487
+ decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
488
+ rescue JWT::InvalidSubError
410
489
  # Handle invalid token, e.g. logout user or deny access
411
490
  end
412
491
  ```
413
492
 
414
- ### Subject Claim
493
+ ### Finding a Key
415
494
 
416
- From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2):
417
-
418
- > The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL.
495
+ 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.
419
496
 
420
497
  ```ruby
421
- sub = 'Subject'
422
- sub_payload = { :data => 'data', :sub => sub }
498
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
499
+ iss_payload = { data: 'data', iss: issuers.first }
423
500
 
424
- token = JWT.encode sub_payload, hmac_secret, 'HS256'
501
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
502
+
503
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
425
504
 
426
505
  begin
427
- # Add sub to the validation to check if the token has been manipulated
428
- decoded_token = JWT.decode token, hmac_secret, true, { 'sub' => sub, :verify_sub => true, :algorithm => 'HS256' }
429
- rescue JWT::InvalidSubError
506
+ # Add iss to the validation to check if the token has been manipulated
507
+ decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
508
+ secrets[payload['iss']]
509
+ end
510
+ rescue JWT::InvalidIssuerError
430
511
  # Handle invalid token, e.g. logout user or deny access
431
512
  end
432
513
  ```
433
514
 
515
+ ### Required Claims
516
+
517
+ You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
518
+ ```ruby
519
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
520
+ JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
521
+ ```
522
+
523
+ ### X.509 certificates in x5c header
524
+
525
+ 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).
526
+
527
+ ```ruby
528
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
529
+ crl_uris = root_certificates.map(&:crl_uris)
530
+ crls = crl_uris.map do |uri|
531
+ # look up cached CRL by `uri` and return it if found, otherwise continue
532
+ crl = Net::HTTP.get(uri)
533
+ crl = OpenSSL::X509::CRL.new(crl)
534
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
535
+ end
536
+
537
+ begin
538
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
539
+ rescue JWT::DecodeError
540
+ # Handle error, e.g. x5c header certificate revoked or expired
541
+ end
542
+ ```
543
+
544
+ ### JSON Web Key (JWK)
545
+
546
+ JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved.
547
+
548
+ If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
549
+
550
+ ```ruby
551
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid')
552
+ payload = { data: 'data' }
553
+ headers = { kid: jwk.kid }
554
+
555
+ token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
556
+
557
+ # The jwk loader would fetch the set of JWKs from a trusted source,
558
+ # to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation.
559
+ # This example only allows cache invalidations every 5 minutes.
560
+ jwk_loader = ->(options) do
561
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
562
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
563
+ @cached_keys = nil
564
+ end
565
+ @cached_keys ||= begin
566
+ @cache_last_update = Time.now.to_i
567
+ { keys: [jwk.export] }
568
+ end
569
+ end
570
+
571
+ begin
572
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader })
573
+ rescue JWT::JWKError
574
+ # Handle problems with the provided JWKs
575
+ rescue JWT::DecodeError
576
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
577
+ end
578
+ ```
579
+
580
+ or by passing the JWKs as a simple Hash
581
+
582
+ ```
583
+ jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
584
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
585
+ ```
586
+
587
+ ### Importing and exporting JSON Web Keys
588
+
589
+ The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.
590
+
591
+ ```ruby
592
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
593
+
594
+ jwk_hash = jwk.export
595
+ jwk_hash_with_private_key = jwk.export(include_private: true)
596
+ ```
597
+
598
+ ### Key ID (kid) and JWKs
599
+
600
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization.
601
+
602
+ ```ruby
603
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
604
+ # OR
605
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
606
+ # OR
607
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint)
608
+
609
+ jwk_hash = jwk.export
610
+
611
+ thumbprint_as_the_kid = jwk_hash[:kid]
612
+
613
+ ```
614
+
434
615
  # Development and Tests
435
616
 
436
617
  We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
@@ -439,40 +620,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
439
620
  rake release
440
621
  ```
441
622
 
442
- The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with
623
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
443
624
 
444
625
  ```bash
445
- bundle exec rspec
626
+ bundle install
627
+ bundle exec appraisal rake test
446
628
  ```
447
629
 
448
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
630
+ ## How to contribute
631
+ See [CONTRIBUTING](CONTRIBUTING.md).
449
632
 
450
633
  ## Contributors
451
634
 
452
- * Jordan Brough <github.jordanb@xoxy.net>
453
- * Ilya Zhitomirskiy <ilya@joindiaspora.com>
454
- * Daniel Grippi <daniel@joindiaspora.com>
455
- * Jeff Lindsay <progrium@gmail.com>
456
- * Bob Aman <bob@sporkmonger.com>
457
- * Micah Gates <github@mgates.com>
458
- * Rob Wygand <rob@wygand.com>
459
- * Ariel Salomon (Oscil8)
460
- * Paul Battley <pbattley@gmail.com>
461
- * Zane Shannon [@zshannon](https://github.com/zshannon)
462
- * Brian Fletcher [@punkle](https://github.com/punkle)
463
- * Alex [@ZhangHanDong](https://github.com/ZhangHanDong)
464
- * John Downey [@jtdowney](https://github.com/jtdowney)
465
- * Adam Greene [@skippy](https://github.com/skippy)
466
- * Tim Rudat [@excpt](https://github.com/excpt) <timrudat@gmail.com> - Maintainer
635
+ See [AUTHORS](AUTHORS).
467
636
 
468
637
  ## License
469
638
 
470
- MIT
471
-
472
- Copyright (c) 2011 Jeff Lindsay
473
-
474
- 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:
475
-
476
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
477
-
478
- 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.
639
+ See [LICENSE](LICENSE).
data/Rakefile CHANGED
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
1
4
  require 'bundler/gem_tasks'
2
5
 
3
6
  begin
4
7
  require 'rspec/core/rake_task'
8
+ require 'rubocop/rake_task'
5
9
 
6
10
  RSpec::Core::RakeTask.new(:test)
11
+ RuboCop::RakeTask.new(:rubocop)
7
12
 
8
- task default: :test
13
+ task default: %i[rubocop test]
9
14
  rescue LoadError
10
15
  puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
11
16
  end