jwt 1.5.4 → 2.5.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 -13
  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 +7 -0
  6. data/.reek.yml +22 -0
  7. data/.rspec +1 -1
  8. data/.rubocop.yml +66 -1
  9. data/.sourcelevel.yml +17 -0
  10. data/AUTHORS +119 -0
  11. data/Appraisals +13 -0
  12. data/CHANGELOG.md +786 -0
  13. data/CODE_OF_CONDUCT.md +84 -0
  14. data/CONTRIBUTING.md +99 -0
  15. data/Gemfile +4 -1
  16. data/README.md +332 -79
  17. data/Rakefile +15 -0
  18. data/lib/jwt/algos/ecdsa.rb +64 -0
  19. data/lib/jwt/algos/eddsa.rb +35 -0
  20. data/lib/jwt/algos/hmac.rb +36 -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 +22 -0
  24. data/lib/jwt/algos/unsupported.rb +19 -0
  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 +119 -30
  33. data/lib/jwt/encode.rb +69 -0
  34. data/lib/jwt/error.rb +10 -0
  35. data/lib/jwt/json.rb +11 -9
  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 +59 -0
  45. data/lib/jwt/signature.rb +35 -0
  46. data/lib/jwt/verify.rb +59 -44
  47. data/lib/jwt/version.rb +8 -3
  48. data/lib/jwt/x5c_key_finder.rb +55 -0
  49. data/lib/jwt.rb +16 -162
  50. data/ruby-jwt.gemspec +14 -8
  51. metadata +71 -84
  52. data/.travis.yml +0 -13
  53. data/Manifest +0 -8
  54. data/spec/fixtures/certs/ec256-private.pem +0 -8
  55. data/spec/fixtures/certs/ec256-public.pem +0 -4
  56. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  57. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  58. data/spec/fixtures/certs/ec384-private.pem +0 -9
  59. data/spec/fixtures/certs/ec384-public.pem +0 -5
  60. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  61. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  62. data/spec/fixtures/certs/ec512-private.pem +0 -10
  63. data/spec/fixtures/certs/ec512-public.pem +0 -6
  64. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  65. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  66. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  67. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  68. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  69. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  70. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  71. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  72. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  73. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  74. data/spec/jwt/verify_spec.rb +0 -175
  75. data/spec/jwt_spec.rb +0 -232
  76. data/spec/spec_helper.rb +0 -31
data/README.md CHANGED
@@ -1,24 +1,34 @@
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=master)](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)
8
+ [![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
7
9
 
8
- 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.
9
11
 
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).
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).
11
13
 
12
14
  ## Announcements
15
+ * Ruby 2.4 support was dropped in version 2.4.0
16
+ * Ruby 1.9.3 support was dropped at December 31st, 2016.
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)
13
18
 
14
- * Ruby 1.9.3 support will be dropped by December 31st, 2016.
15
- * Version 1.5.3 yanked. See: #132 and #133
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)|
16
26
 
17
27
  ## Installing
18
28
 
19
29
  ### Using Rubygems:
20
30
  ```bash
21
- sudo gem install jwt
31
+ gem install jwt
22
32
  ```
23
33
 
24
34
  ### Using Bundler:
@@ -30,23 +40,23 @@ And run `bundle install`
30
40
 
31
41
  ## Algorithms and Usage
32
42
 
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/).
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**
34
44
 
35
45
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
36
46
 
37
- **NONE**
47
+ ### **NONE**
38
48
 
39
49
  * none - unsigned token
40
50
 
41
51
  ```ruby
42
52
  require 'jwt'
43
53
 
44
- payload = {:data => 'test'}
54
+ payload = { data: 'test' }
45
55
 
46
56
  # IMPORTANT: set nil as password parameter
47
57
  token = JWT.encode payload, nil, 'none'
48
58
 
49
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ0ZXN0IjoiZGF0YSJ9.
59
+ # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
50
60
  puts token
51
61
 
52
62
  # Set password to nil and validation to false otherwise this won't work
@@ -55,36 +65,44 @@ decoded_token = JWT.decode token, nil, false
55
65
  # Array
56
66
  # [
57
67
  # {"data"=>"test"}, # payload
58
- # {"typ"=>"JWT", "alg"=>"none"} # header
68
+ # {"alg"=>"none"} # header
59
69
  # ]
60
70
  puts decoded_token
61
71
  ```
62
72
 
63
- **HMAC** (default: HS256)
73
+ ### **HMAC**
64
74
 
65
- * HS256 - HMAC using SHA-256 hash algorithm (default)
66
- * HS384 - HMAC using SHA-384 hash algorithm
75
+ * HS256 - HMAC using SHA-256 hash algorithm
76
+ * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
77
+ * HS384 - HMAC using SHA-384 hash algorithm
67
78
  * HS512 - HMAC using SHA-512 hash algorithm
68
79
 
69
80
  ```ruby
81
+ # The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
70
82
  hmac_secret = 'my$ecretK3y'
71
83
 
72
84
  token = JWT.encode payload, hmac_secret, 'HS256'
73
85
 
74
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9._sLPAGP-IXgho8BkMGQ86N2mah7vDyn0L5hOR4UkfoI
86
+ # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
75
87
  puts token
76
88
 
77
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
89
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
78
90
 
79
91
  # Array
80
92
  # [
81
93
  # {"data"=>"test"}, # payload
82
- # {"typ"=>"JWT", "alg"=>"HS256"} # header
94
+ # {"alg"=>"HS256"} # header
83
95
  # ]
84
96
  puts decoded_token
85
97
  ```
86
98
 
87
- **RSA**
99
+ Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl enforces a maximum key size of 32 bytes for these algorithms.
100
+
101
+ [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
102
+ [libsodium](https://github.com/jedisct1/libsodium), it can be installed
103
+ on MacOS with `brew install libsodium`.
104
+
105
+ ### **RSA**
88
106
 
89
107
  * RS256 - RSA using SHA-256 hash algorithm
90
108
  * RS384 - RSA using SHA-384 hash algorithm
@@ -96,49 +114,103 @@ rsa_public = rsa_private.public_key
96
114
 
97
115
  token = JWT.encode payload, rsa_private, 'RS256'
98
116
 
99
- # 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
100
118
  puts token
101
119
 
102
- decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
120
+ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
103
121
 
104
122
  # Array
105
123
  # [
106
124
  # {"data"=>"test"}, # payload
107
- # {"typ"=>"JWT", "alg"=>"RS256"} # header
125
+ # {"alg"=>"RS256"} # header
108
126
  # ]
109
127
  puts decoded_token
110
128
  ```
111
129
 
112
- **ECDSA**
130
+ ### **ECDSA**
113
131
 
114
132
  * ES256 - ECDSA using P-256 and SHA-256
115
133
  * ES384 - ECDSA using P-384 and SHA-384
116
134
  * ES512 - ECDSA using P-521 and SHA-512
135
+ * ES256K - ECDSA using P-256K and SHA-256
117
136
 
118
137
  ```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
138
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
123
139
 
124
140
  token = JWT.encode payload, ecdsa_key, 'ES256'
125
141
 
126
- # eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.MEQCIAtShrxRwP1L9SapqaT4f7hajDJH4t_rfm-YlZcNDsBNAiB64M4-JRfyS8nRMlywtQ9lHbvvec9U54KznzOe1YxTyA
142
+ # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
127
143
  puts token
128
144
 
129
- decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' }
145
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
130
146
 
131
147
  # Array
132
148
  # [
133
149
  # {"test"=>"data"}, # payload
134
- # {"typ"=>"JWT", "alg"=>"ES256"} # header
150
+ # {"alg"=>"ES256"} # header
135
151
  # ]
136
152
  puts decoded_token
137
153
  ```
138
154
 
139
- **RSASSA-PSS**
155
+ ### **EDDSA**
156
+
157
+ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
158
+
159
+ ```ruby
160
+ gem 'rbnacl'
161
+ ```
162
+
163
+ For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
164
+
165
+ * ED25519
166
+
167
+ ```ruby
168
+ private_key = RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF')
169
+ public_key = private_key.verify_key
170
+ token = JWT.encode payload, private_key, 'ED25519'
171
+
172
+ # eyJhbGciOiJFRDI1NTE5In0.eyJkYXRhIjoidGVzdCJ9.6xIztXyOupskddGA_RvKU76V9b2dCQUYhoZEVFnRimJoPYIzZ2Fm47CWw8k2NTCNpgfAuxg9OXjaiVK7MvrbCQ
173
+ puts token
174
+
175
+ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
176
+ # Array
177
+ # [
178
+ # {"test"=>"data"}, # payload
179
+ # {"alg"=>"ED25519"} # header
180
+ # ]
181
+
182
+ ```
183
+
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' }
140
206
 
141
- Not implemented.
207
+ # Array
208
+ # [
209
+ # {"data"=>"test"}, # payload
210
+ # {"alg"=>"PS256"} # header
211
+ # ]
212
+ puts decoded_token
213
+ ```
142
214
 
143
215
  ## Support for reserved claim names
144
216
  JSON Web Token defines some reserved claim names and defines how they should be
@@ -152,6 +224,38 @@ used. JWT supports these reserved claim names:
152
224
  - 'iat' (Issued At) Claim
153
225
  - 'sub' (Subject) Claim
154
226
 
227
+ ## Add custom header fields
228
+ Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
229
+ To add custom header fields you need to pass `header_fields` parameter
230
+
231
+ ```ruby
232
+ token = JWT.encode payload, key, algorithm='HS256', header_fields={}
233
+ ```
234
+
235
+ **Example:**
236
+
237
+ ```ruby
238
+ require 'jwt'
239
+
240
+ payload = { data: 'test' }
241
+
242
+ # IMPORTANT: set nil as password parameter
243
+ token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
244
+
245
+ # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
246
+ puts token
247
+
248
+ # Set password to nil and validation to false otherwise this won't work
249
+ decoded_token = JWT.decode token, nil, false
250
+
251
+ # Array
252
+ # [
253
+ # {"data"=>"test"}, # payload
254
+ # {"typ"=>"JWT", "alg"=>"none"} # header
255
+ # ]
256
+ puts decoded_token
257
+ ```
258
+
155
259
  ### Expiration Time Claim
156
260
 
157
261
  From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4):
@@ -162,31 +266,37 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
162
266
 
163
267
  ```ruby
164
268
  exp = Time.now.to_i + 4 * 3600
165
- exp_payload = { :data => 'data', :exp => exp }
269
+ exp_payload = { data: 'data', exp: exp }
166
270
 
167
271
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
168
272
 
169
273
  begin
170
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
274
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
171
275
  rescue JWT::ExpiredSignature
172
276
  # Handle expired token, e.g. logout user or deny access
173
277
  end
174
278
  ```
175
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
+
176
286
  **Adding Leeway**
177
287
 
178
288
  ```ruby
179
289
  exp = Time.now.to_i - 10
180
290
  leeway = 30 # seconds
181
291
 
182
- exp_payload = { :data => 'data', :exp => exp }
292
+ exp_payload = { data: 'data', exp: exp }
183
293
 
184
294
  # build expired token
185
295
  token = JWT.encode exp_payload, hmac_secret, 'HS256'
186
296
 
187
297
  begin
188
298
  # add leeway to ensure the token is still accepted
189
- decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' }
299
+ decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
190
300
  rescue JWT::ExpiredSignature
191
301
  # Handle expired token, e.g. logout user or deny access
192
302
  end
@@ -202,31 +312,37 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
202
312
 
203
313
  ```ruby
204
314
  nbf = Time.now.to_i - 3600
205
- nbf_payload = { :data => 'data', :nbf => nbf }
315
+ nbf_payload = { data: 'data', nbf: nbf }
206
316
 
207
317
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
208
318
 
209
319
  begin
210
- decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
320
+ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
211
321
  rescue JWT::ImmatureSignature
212
322
  # Handle invalid token, e.g. logout user or deny access
213
323
  end
214
324
  ```
215
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
+
216
332
  **Adding Leeway**
217
333
 
218
334
  ```ruby
219
335
  nbf = Time.now.to_i + 10
220
336
  leeway = 30
221
337
 
222
- nbf_payload = { :data => 'data', :nbf => nbf }
338
+ nbf_payload = { data: 'data', nbf: nbf }
223
339
 
224
340
  # build expired token
225
341
  token = JWT.encode nbf_payload, hmac_secret, 'HS256'
226
342
 
227
343
  begin
228
344
  # add leeway to ensure the token is valid
229
- decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' }
345
+ decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
230
346
  rescue JWT::ImmatureSignature
231
347
  # Handle invalid token, e.g. logout user or deny access
232
348
  end
@@ -238,20 +354,52 @@ From [Oauth JSON Web Token 4.1.1. "iss" (Issuer) Claim](https://tools.ietf.org/h
238
354
 
239
355
  > 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
356
 
357
+ You can pass multiple allowed issuers as an Array, verification will pass if one of them matches the `iss` value in the payload.
358
+
241
359
  ```ruby
242
360
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
243
- iss_payload = { :data => 'data', :iss => iss }
361
+ iss_payload = { data: 'data', iss: iss }
244
362
 
245
363
  token = JWT.encode iss_payload, hmac_secret, 'HS256'
246
364
 
247
365
  begin
248
366
  # 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' }
367
+ decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
250
368
  rescue JWT::InvalidIssuerError
251
369
  # Handle invalid token, e.g. logout user or deny access
252
370
  end
253
371
  ```
254
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
+
255
403
  ### Audience Claim
256
404
 
257
405
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -260,13 +408,13 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
260
408
 
261
409
  ```ruby
262
410
  aud = ['Young', 'Old']
263
- aud_payload = { :data => 'data', :aud => aud }
411
+ aud_payload = { data: 'data', aud: aud }
264
412
 
265
413
  token = JWT.encode aud_payload, hmac_secret, 'HS256'
266
414
 
267
415
  begin
268
416
  # 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' }
417
+ decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
270
418
  rescue JWT::InvalidAudError
271
419
  # Handle invalid token, e.g. logout user or deny access
272
420
  puts 'Audience Error'
@@ -283,37 +431,40 @@ From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/h
283
431
  # Use the secret and iat to create a unique key per request to prevent replay attacks
284
432
  jti_raw = [hmac_secret, iat].join(':').to_s
285
433
  jti = Digest::MD5.hexdigest(jti_raw)
286
- jti_payload = { :data => 'data', :iat => iat, :jti => jti }
434
+ jti_payload = { data: 'data', iat: iat, jti: jti }
287
435
 
288
436
  token = JWT.encode jti_payload, hmac_secret, 'HS256'
289
437
 
290
438
  begin
291
439
  # 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' }
440
+ #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
293
441
  # 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' }
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' }
295
445
  rescue JWT::InvalidJtiError
296
446
  # Handle invalid token, e.g. logout user or deny access
297
447
  puts 'Error'
298
448
  end
299
-
300
449
  ```
301
450
 
302
451
  ### Issued At Claim
303
452
 
304
453
  From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
305
454
 
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.
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.
456
+
457
+ **Handle Issued At Claim**
307
458
 
308
459
  ```ruby
309
460
  iat = Time.now.to_i
310
- iat_payload = { :data => 'data', :iat => iat }
461
+ iat_payload = { data: 'data', iat: iat }
311
462
 
312
463
  token = JWT.encode iat_payload, hmac_secret, 'HS256'
313
464
 
314
465
  begin
315
466
  # 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' }
467
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
317
468
  rescue JWT::InvalidIatError
318
469
  # Handle invalid token, e.g. logout user or deny access
319
470
  end
@@ -327,18 +478,140 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
327
478
 
328
479
  ```ruby
329
480
  sub = 'Subject'
330
- sub_payload = { :data => 'data', :sub => sub }
481
+ sub_payload = { data: 'data', sub: sub }
331
482
 
332
483
  token = JWT.encode sub_payload, hmac_secret, 'HS256'
333
484
 
334
485
  begin
335
486
  # 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' }
487
+ decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
337
488
  rescue JWT::InvalidSubError
338
489
  # Handle invalid token, e.g. logout user or deny access
339
490
  end
340
491
  ```
341
492
 
493
+ ### Finding a Key
494
+
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.
496
+
497
+ ```ruby
498
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
499
+ iss_payload = { data: 'data', iss: issuers.first }
500
+
501
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
502
+
503
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
504
+
505
+ begin
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
511
+ # Handle invalid token, e.g. logout user or deny access
512
+ end
513
+ ```
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
+
342
615
  # Development and Tests
343
616
 
344
617
  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 +620,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
347
620
  rake release
348
621
  ```
349
622
 
350
- 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.
351
624
 
352
625
  ```bash
353
- bundle exec rspec
626
+ bundle install
627
+ bundle exec appraisal rake test
354
628
  ```
355
629
 
356
- **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).
357
632
 
358
633
  ## Contributors
359
634
 
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
635
+ See [AUTHORS](AUTHORS).
375
636
 
376
637
  ## License
377
638
 
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.
639
+ See [LICENSE](LICENSE).