jwt 2.3.0 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +60 -53
  3. data/CHANGELOG.md +194 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +360 -106
  7. data/lib/jwt/base64.rb +19 -2
  8. data/lib/jwt/claims/audience.rb +30 -0
  9. data/lib/jwt/claims/crit.rb +35 -0
  10. data/lib/jwt/claims/decode_verifier.rb +40 -0
  11. data/lib/jwt/claims/expiration.rb +32 -0
  12. data/lib/jwt/claims/issued_at.rb +22 -0
  13. data/lib/jwt/claims/issuer.rb +34 -0
  14. data/lib/jwt/claims/jwt_id.rb +35 -0
  15. data/lib/jwt/claims/not_before.rb +32 -0
  16. data/lib/jwt/claims/numeric.rb +77 -0
  17. data/lib/jwt/claims/required.rb +33 -0
  18. data/lib/jwt/claims/subject.rb +30 -0
  19. data/lib/jwt/claims/verification_methods.rb +20 -0
  20. data/lib/jwt/claims/verifier.rb +61 -0
  21. data/lib/jwt/claims.rb +74 -0
  22. data/lib/jwt/claims_validator.rb +7 -24
  23. data/lib/jwt/configuration/container.rb +52 -0
  24. data/lib/jwt/configuration/decode_configuration.rb +70 -0
  25. data/lib/jwt/configuration/jwk_configuration.rb +28 -0
  26. data/lib/jwt/configuration.rb +23 -0
  27. data/lib/jwt/decode.rb +70 -61
  28. data/lib/jwt/deprecations.rb +49 -0
  29. data/lib/jwt/encode.rb +18 -57
  30. data/lib/jwt/encoded_token.rb +139 -0
  31. data/lib/jwt/error.rb +36 -0
  32. data/lib/jwt/json.rb +1 -1
  33. data/lib/jwt/jwa/compat.rb +32 -0
  34. data/lib/jwt/jwa/ecdsa.rb +90 -0
  35. data/lib/jwt/jwa/eddsa.rb +35 -0
  36. data/lib/jwt/jwa/hmac.rb +82 -0
  37. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  38. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
  39. data/lib/jwt/jwa/none.rb +24 -0
  40. data/lib/jwt/jwa/ps.rb +35 -0
  41. data/lib/jwt/jwa/rsa.rb +35 -0
  42. data/lib/jwt/jwa/signing_algorithm.rb +63 -0
  43. data/lib/jwt/jwa/unsupported.rb +20 -0
  44. data/lib/jwt/jwa/wrapper.rb +44 -0
  45. data/lib/jwt/jwa.rb +58 -0
  46. data/lib/jwt/jwk/ec.rb +163 -63
  47. data/lib/jwt/jwk/hmac.rb +68 -24
  48. data/lib/jwt/jwk/key_base.rb +46 -6
  49. data/lib/jwt/jwk/key_finder.rb +20 -35
  50. data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
  51. data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
  52. data/lib/jwt/jwk/rsa.rb +141 -54
  53. data/lib/jwt/jwk/set.rb +82 -0
  54. data/lib/jwt/jwk/thumbprint.rb +26 -0
  55. data/lib/jwt/jwk.rb +16 -11
  56. data/lib/jwt/token.rb +112 -0
  57. data/lib/jwt/verify.rb +16 -81
  58. data/lib/jwt/version.rb +53 -11
  59. data/lib/jwt/x5c_key_finder.rb +52 -0
  60. data/lib/jwt.rb +28 -4
  61. data/ruby-jwt.gemspec +15 -5
  62. metadata +75 -28
  63. data/.github/workflows/test.yml +0 -74
  64. data/.gitignore +0 -11
  65. data/.rspec +0 -2
  66. data/.rubocop.yml +0 -97
  67. data/.rubocop_todo.yml +0 -185
  68. data/.sourcelevel.yml +0 -18
  69. data/Appraisals +0 -10
  70. data/Gemfile +0 -5
  71. data/Rakefile +0 -14
  72. data/lib/jwt/algos/ecdsa.rb +0 -35
  73. data/lib/jwt/algos/eddsa.rb +0 -30
  74. data/lib/jwt/algos/hmac.rb +0 -34
  75. data/lib/jwt/algos/none.rb +0 -15
  76. data/lib/jwt/algos/ps.rb +0 -43
  77. data/lib/jwt/algos/rsa.rb +0 -19
  78. data/lib/jwt/algos/unsupported.rb +0 -17
  79. data/lib/jwt/algos.rb +0 -44
  80. data/lib/jwt/default_options.rb +0 -16
  81. data/lib/jwt/security_utils.rb +0 -57
  82. data/lib/jwt/signature.rb +0 -39
data/README.md CHANGED
@@ -1,20 +1,38 @@
1
1
  # JWT
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt)
4
- [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](https://github.com/jwt/ruby-jwt/actions)
4
+ [![Build Status](https://github.com/jwt/ruby-jwt/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/jwt/ruby-jwt/actions)
5
5
  [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
6
6
  [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
7
7
  [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
8
- [![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
9
8
 
10
9
  A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
11
10
 
12
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).
13
12
 
14
- ## Announcements
13
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
15
14
 
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)
15
+ ## Upcoming breaking changes
16
+
17
+ Notable changes in the upcoming **version 3.0**:
18
+
19
+ - The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed:
20
+ - Support for the non-standard SHA512256 algorithm will be removed.
21
+ - Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling.
22
+
23
+ - Base64 decoding will no longer fallback on the looser RFC 2045.
24
+
25
+ - Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new api](https://github.com/jwt/ruby-jwt/pull/626) and lead to the following deprecations:
26
+ - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`.
27
+ - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`.
28
+ - The `::JWT::JWA.create` method will be removed.
29
+ - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`.
30
+ - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
31
+ - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
32
+
33
+ - The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements:
34
+ - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed.
35
+ - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module.
18
36
 
19
37
  ## Sponsors
20
38
 
@@ -25,40 +43,68 @@ If you have further questions related to development or usage, join us: [ruby-jw
25
43
  ## Installing
26
44
 
27
45
  ### Using Rubygems:
46
+
28
47
  ```bash
29
48
  gem install jwt
30
49
  ```
31
50
 
32
51
  ### Using Bundler:
52
+
33
53
  Add the following to your Gemfile
34
54
  ```
35
55
  gem 'jwt'
36
56
  ```
57
+
37
58
  And run `bundle install`
38
59
 
60
+ Finally require the gem in your application
61
+ ```ruby
62
+ require 'jwt'
63
+ ```
64
+
39
65
  ## Algorithms and Usage
40
66
 
41
- The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
67
+ The jwt gem natively supports the NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms via the openssl library. The gem can be extended with additional or alternative implementations of the algorithms via extensions.
68
+
69
+ Additionally the EdDSA algorithm is supported via a [separate gem](https://rubygems.org/gems/jwt-eddsa).
70
+
71
+ For safe cryptographic signing, you need to specify the algorithm in the options hash whenever you call `JWT.decode` to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
42
72
 
43
73
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
44
74
 
45
- **NONE**
75
+ ### Deprecation warnings
76
+
77
+ Deprecation warnings are logged once (`:once` option) by default to avoid spam in logs. Other options are `:silent` to completely silence warnings and `:warn` to log every time a deprecated path is executed.
78
+
79
+ ```ruby
80
+ JWT.configuration.deprecation_warnings = :warn # default is :once
81
+ ```
82
+
83
+ ### Base64 decoding
84
+
85
+ In the past the gem has been supporting the Base64 decoding specified in [RFC2045](https://www.rfc-editor.org/rfc/rfc2045) allowing newlines and blanks in the base64 encoded payload. In future versions base64 decoding will be stricter and only comply to [RFC4648](https://www.rfc-editor.org/rfc/rfc4648).
86
+
87
+ The stricter base64 decoding when processing tokens can be done via the `strict_base64_decoding` configuration accessor.
88
+ ```ruby
89
+ JWT.configuration.strict_base64_decoding = true # default is false
90
+ ```
91
+
92
+ ### **NONE**
46
93
 
47
94
  * none - unsigned token
48
95
 
49
96
  ```ruby
50
- require 'jwt'
51
97
 
52
98
  payload = { data: 'test' }
53
99
 
54
100
  # IMPORTANT: set nil as password parameter
55
- token = JWT.encode payload, nil, 'none'
101
+ token = JWT.encode(payload, nil, 'none')
56
102
 
57
103
  # eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.
58
104
  puts token
59
105
 
60
106
  # Set password to nil and validation to false otherwise this won't work
61
- decoded_token = JWT.decode token, nil, false
107
+ decoded_token = JWT.decode(token, nil, false)
62
108
 
63
109
  # Array
64
110
  # [
@@ -68,23 +114,22 @@ decoded_token = JWT.decode token, nil, false
68
114
  puts decoded_token
69
115
  ```
70
116
 
71
- **HMAC**
117
+ ### **HMAC**
72
118
 
73
119
  * HS256 - HMAC using SHA-256 hash algorithm
74
- * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
75
120
  * HS384 - HMAC using SHA-384 hash algorithm
76
121
  * HS512 - HMAC using SHA-512 hash algorithm
77
122
 
78
123
  ```ruby
79
- # The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
124
+ # The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided.
80
125
  hmac_secret = 'my$ecretK3y'
81
126
 
82
- token = JWT.encode payload, hmac_secret, 'HS256'
127
+ token = JWT.encode(payload, hmac_secret, 'HS256')
83
128
 
84
129
  # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y
85
130
  puts token
86
131
 
87
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
132
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
88
133
 
89
134
  # Array
90
135
  # [
@@ -94,28 +139,22 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
94
139
  puts decoded_token
95
140
  ```
96
141
 
97
- 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.
98
-
99
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
100
- [libsodium](https://github.com/jedisct1/libsodium), it can be installed
101
- on MacOS with `brew install libsodium`.
102
-
103
- **RSA**
142
+ ### **RSA**
104
143
 
105
144
  * RS256 - RSA using SHA-256 hash algorithm
106
145
  * RS384 - RSA using SHA-384 hash algorithm
107
146
  * RS512 - RSA using SHA-512 hash algorithm
108
147
 
109
148
  ```ruby
110
- rsa_private = OpenSSL::PKey::RSA.generate 2048
149
+ rsa_private = OpenSSL::PKey::RSA.generate(2048)
111
150
  rsa_public = rsa_private.public_key
112
151
 
113
- token = JWT.encode payload, rsa_private, 'RS256'
152
+ token = JWT.encode(payload, rsa_private, 'RS256')
114
153
 
115
154
  # eyJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.GplO4w1spRgvEJQ3-FOtZr-uC8L45Jt7SN0J4woBnEXG_OZBSNcZjAJWpjadVYEe2ev3oUBFDYM1N_-0BTVeFGGYvMewu8E6aMjSZvOpf1cZBew-Vt4poSq7goG2YRI_zNPt3af2lkPqXD796IKC5URrEvcgF5xFQ-6h07XRDpSRx1ECrNsUOt7UM3l1IB4doY11GzwQA5sHDTmUZ0-kBT76ZMf12Srg_N3hZwphxBtudYtN5VGZn420sVrQMdPE_7Ni3EiWT88j7WCr1xrF60l8sZT3yKCVleG7D2BEXacTntB7GktBv4Xo8OKnpwpqTpIlC05dMowMkz3rEAAYbQ
116
155
  puts token
117
156
 
118
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
157
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
119
158
 
120
159
  # Array
121
160
  # [
@@ -125,24 +164,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
125
164
  puts decoded_token
126
165
  ```
127
166
 
128
- **ECDSA**
167
+ ### **ECDSA**
129
168
 
130
169
  * ES256 - ECDSA using P-256 and SHA-256
131
170
  * ES384 - ECDSA using P-384 and SHA-384
132
171
  * ES512 - ECDSA using P-521 and SHA-512
172
+ * ES256K - ECDSA using P-256K and SHA-256
133
173
 
134
174
  ```ruby
135
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
136
- ecdsa_key.generate_key
137
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
138
- ecdsa_public.private_key = nil
175
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
139
176
 
140
- token = JWT.encode payload, ecdsa_key, 'ES256'
177
+ token = JWT.encode(payload, ecdsa_key, 'ES256')
141
178
 
142
179
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
143
180
  puts token
144
181
 
145
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
182
+ decoded_token = JWT.decode(token, ecdsa_key, true, { algorithm: 'ES256' })
146
183
 
147
184
  # Array
148
185
  # [
@@ -152,7 +189,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
152
189
  puts decoded_token
153
190
  ```
154
191
 
155
- **EDDSA**
192
+ ### **EDDSA**
156
193
 
157
194
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
158
195
 
@@ -160,7 +197,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
160
197
  gem 'rbnacl'
161
198
  ```
162
199
 
163
- For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
200
+ For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
164
201
 
165
202
  * ED25519
166
203
 
@@ -181,9 +218,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
181
218
 
182
219
  ```
183
220
 
184
- **RSASSA-PSS**
221
+ ### **RSASSA-PSS**
185
222
 
186
- In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`.
223
+ 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
224
 
188
225
  ```ruby
189
226
  gem 'openssl', '~> 2.1'
@@ -197,12 +234,12 @@ gem 'openssl', '~> 2.1'
197
234
  rsa_private = OpenSSL::PKey::RSA.generate 2048
198
235
  rsa_public = rsa_private.public_key
199
236
 
200
- token = JWT.encode payload, rsa_private, 'PS256'
237
+ token = JWT.encode(payload, rsa_private, 'PS256')
201
238
 
202
239
  # eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
203
240
  puts token
204
241
 
205
- decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
242
+ decoded_token = JWT.decode(token, rsa_public, true, { algorithm: 'PS256' })
206
243
 
207
244
  # Array
208
245
  # [
@@ -212,41 +249,28 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
212
249
  puts decoded_token
213
250
  ```
214
251
 
215
- ## Support for reserved claim names
216
- JSON Web Token defines some reserved claim names and defines how they should be
217
- used. JWT supports these reserved claim names:
218
-
219
- - 'exp' (Expiration Time) Claim
220
- - 'nbf' (Not Before Time) Claim
221
- - 'iss' (Issuer) Claim
222
- - 'aud' (Audience) Claim
223
- - 'jti' (JWT ID) Claim
224
- - 'iat' (Issued At) Claim
225
- - 'sub' (Subject) Claim
226
-
227
- ## Add custom header fields
252
+ ### Add custom header fields
228
253
  Ruby-jwt gem supports custom [header fields](https://tools.ietf.org/html/rfc7519#section-5)
229
254
  To add custom header fields you need to pass `header_fields` parameter
230
255
 
231
256
  ```ruby
232
- token = JWT.encode payload, key, algorithm='HS256', header_fields={}
257
+ token = JWT.encode(payload, key, algorithm='HS256', header_fields={})
233
258
  ```
234
259
 
235
260
  **Example:**
236
261
 
237
262
  ```ruby
238
- require 'jwt'
239
263
 
240
264
  payload = { data: 'test' }
241
265
 
242
266
  # IMPORTANT: set nil as password parameter
243
- token = JWT.encode payload, nil, 'none', { typ: 'JWT' }
267
+ token = JWT.encode(payload, nil, 'none', { typ: 'JWT' })
244
268
 
245
269
  # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjoidGVzdCJ9.
246
270
  puts token
247
271
 
248
272
  # Set password to nil and validation to false otherwise this won't work
249
- decoded_token = JWT.decode token, nil, false
273
+ decoded_token = JWT.decode(token, nil, false)
250
274
 
251
275
  # Array
252
276
  # [
@@ -256,6 +280,95 @@ decoded_token = JWT.decode token, nil, false
256
280
  puts decoded_token
257
281
  ```
258
282
 
283
+ ### **Custom algorithms**
284
+
285
+ When encoding or decoding a token, you can pass in a custom object through the `algorithm` option to handle signing or verification. This custom object must include or extend the `JWT::JWA::SigningAlgorithm` module and implement certain methods:
286
+
287
+ - For decoding/verifying: The object must implement the methods `alg` and `verify`.
288
+ - For encoding/signing: The object must implement the methods `alg` and `sign`.
289
+
290
+ For customization options check the details from `JWT::JWA::SigningAlgorithm`.
291
+
292
+
293
+ ```ruby
294
+ module CustomHS512Algorithm
295
+ extend JWT::JWA::SigningAlgorithm
296
+
297
+ def self.alg
298
+ 'HS512'
299
+ end
300
+
301
+ def self.sign(data:, signing_key:)
302
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data)
303
+ end
304
+
305
+ def self.verify(data:, signature:, verification_key:)
306
+ ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature)
307
+ end
308
+ end
309
+
310
+ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
311
+ payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
312
+ ```
313
+
314
+ ## `JWT::Token` and `JWT::EncodedToken`
315
+
316
+ The `JWT::Token` and `JWT::EncodedToken` classes can be used to manage your JWTs.
317
+
318
+ ```ruby
319
+ token = JWT::Token.new(payload: { exp: Time.now.to_i + 60, jti: '1234', sub: "my-subject" }, header: { kid: 'hmac' })
320
+ token.sign!(algorithm: 'HS256', key: "secret")
321
+
322
+ token.jwt # => "eyJhbGciOiJIUzI1N..."
323
+ ```
324
+
325
+ The `JWT::EncodedToken` can be used to create a token object that allows verification of signatures and claims
326
+ ```ruby
327
+ encoded_token = JWT::EncodedToken.new(token.jwt)
328
+
329
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
330
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "wrong_secret") # raises JWT::VerificationError
331
+ encoded_token.verify_claims!(:exp, :jti)
332
+ encoded_token.verify_claims!(sub: ["not-my-subject"]) # raises JWT::InvalidSubError
333
+ encoded_token.claim_errors(sub: ["not-my-subject"]).map(&:message) # => ["Invalid subject. Expected [\"not-my-subject\"], received my-subject"]
334
+ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
335
+ encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
336
+ ```
337
+
338
+ ### Detached payload
339
+
340
+ The `::JWT::Token#detach_payload!` method can be use to detach the payload from the JWT.
341
+
342
+ ```ruby
343
+ token = JWT::Token.new(payload: { pay: 'load' })
344
+ token.sign!(algorithm: 'HS256', key: "secret")
345
+ token.detach_payload!
346
+ token.jwt # => "eyJhbGciOiJIUzI1NiJ9..UEhDY1Qlj29ammxuVRA_-gBah4qTy5FngIWg0yEAlC0"
347
+ token.encoded_payload # => "eyJwYXkiOiJsb2FkIn0"
348
+ ```
349
+
350
+ The `JWT::EncodedToken` class can be used to decode a token with a detached payload by providing the payload to the token instance in separate.
351
+
352
+ ```ruby
353
+ encoded_token = JWT::EncodedToken.new(token.jwt)
354
+ encoded_token.encoded_payload = "eyJwYXkiOiJsb2FkIn0"
355
+ encoded_token.verify_signature!(algorithm: 'HS256', key: "secret")
356
+ encoded_token.payload # => {"pay"=>"load"}
357
+ ```
358
+
359
+ ## Claims
360
+
361
+ JSON Web Token defines some reserved claim names and defines how they should be
362
+ used. JWT supports these reserved claim names:
363
+
364
+ - 'exp' (Expiration Time) Claim
365
+ - 'nbf' (Not Before Time) Claim
366
+ - 'iss' (Issuer) Claim
367
+ - 'aud' (Audience) Claim
368
+ - 'jti' (JWT ID) Claim
369
+ - 'iat' (Issued At) Claim
370
+ - 'sub' (Subject) Claim
371
+
259
372
  ### Expiration Time Claim
260
373
 
261
374
  From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4):
@@ -268,10 +381,10 @@ From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.i
268
381
  exp = Time.now.to_i + 4 * 3600
269
382
  exp_payload = { data: 'data', exp: exp }
270
383
 
271
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
384
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
272
385
 
273
386
  begin
274
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
387
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
275
388
  rescue JWT::ExpiredSignature
276
389
  # Handle expired token, e.g. logout user or deny access
277
390
  end
@@ -280,7 +393,7 @@ end
280
393
  The Expiration Claim verification can be disabled.
281
394
  ```ruby
282
395
  # Decode token without raising JWT::ExpiredSignature error
283
- JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
396
+ JWT.decode(token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' })
284
397
  ```
285
398
 
286
399
  **Adding Leeway**
@@ -292,11 +405,11 @@ leeway = 30 # seconds
292
405
  exp_payload = { data: 'data', exp: exp }
293
406
 
294
407
  # build expired token
295
- token = JWT.encode exp_payload, hmac_secret, 'HS256'
408
+ token = JWT.encode(exp_payload, hmac_secret, 'HS256')
296
409
 
297
410
  begin
298
411
  # add leeway to ensure the token is still accepted
299
- decoded_token = JWT.decode token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' }
412
+ decoded_token = JWT.decode(token, hmac_secret, true, { exp_leeway: leeway, algorithm: 'HS256' })
300
413
  rescue JWT::ExpiredSignature
301
414
  # Handle expired token, e.g. logout user or deny access
302
415
  end
@@ -314,10 +427,10 @@ From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.o
314
427
  nbf = Time.now.to_i - 3600
315
428
  nbf_payload = { data: 'data', nbf: nbf }
316
429
 
317
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
430
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
318
431
 
319
432
  begin
320
- decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
433
+ decoded_token = JWT.decode(token, hmac_secret, true, { algorithm: 'HS256' })
321
434
  rescue JWT::ImmatureSignature
322
435
  # Handle invalid token, e.g. logout user or deny access
323
436
  end
@@ -326,7 +439,7 @@ end
326
439
  The Not Before Claim verification can be disabled.
327
440
  ```ruby
328
441
  # Decode token without raising JWT::ImmatureSignature error
329
- JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
442
+ JWT.decode(token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' })
330
443
  ```
331
444
 
332
445
  **Adding Leeway**
@@ -338,11 +451,11 @@ leeway = 30
338
451
  nbf_payload = { data: 'data', nbf: nbf }
339
452
 
340
453
  # build expired token
341
- token = JWT.encode nbf_payload, hmac_secret, 'HS256'
454
+ token = JWT.encode(nbf_payload, hmac_secret, 'HS256')
342
455
 
343
456
  begin
344
457
  # add leeway to ensure the token is valid
345
- decoded_token = JWT.decode token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' }
458
+ decoded_token = JWT.decode(token, hmac_secret, true, { nbf_leeway: leeway, algorithm: 'HS256' })
346
459
  rescue JWT::ImmatureSignature
347
460
  # Handle invalid token, e.g. logout user or deny access
348
461
  end
@@ -360,16 +473,46 @@ You can pass multiple allowed issuers as an Array, verification will pass if one
360
473
  iss = 'My Awesome Company Inc. or https://my.awesome.website/'
361
474
  iss_payload = { data: 'data', iss: iss }
362
475
 
363
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
476
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
364
477
 
365
478
  begin
366
479
  # Add iss to the validation to check if the token has been manipulated
367
- decoded_token = JWT.decode token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }
480
+ decoded_token = JWT.decode(token, hmac_secret, true, { iss: iss, verify_iss: true, algorithm: 'HS256' })
368
481
  rescue JWT::InvalidIssuerError
369
482
  # Handle invalid token, e.g. logout user or deny access
370
483
  end
371
484
  ```
372
485
 
486
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
487
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
488
+ to convert them to proc (using `to_proc`)
489
+
490
+ ```ruby
491
+ JWT.decode(token, hmac_secret, true,
492
+ iss: %r'https://my.awesome.website/',
493
+ verify_iss: true,
494
+ algorithm: 'HS256')
495
+ ```
496
+
497
+ ```ruby
498
+ JWT.decode(token, hmac_secret, true,
499
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
500
+ verify_iss: true,
501
+ algorithm: 'HS256')
502
+ ```
503
+
504
+ ```ruby
505
+ JWT.decode(token, hmac_secret, true,
506
+ iss: method(:valid_issuer?),
507
+ verify_iss: true,
508
+ algorithm: 'HS256')
509
+
510
+ # somewhere in the same class:
511
+ def valid_issuer?(issuer)
512
+ # custom validation
513
+ end
514
+ ```
515
+
373
516
  ### Audience Claim
374
517
 
375
518
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -380,11 +523,11 @@ From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org
380
523
  aud = ['Young', 'Old']
381
524
  aud_payload = { data: 'data', aud: aud }
382
525
 
383
- token = JWT.encode aud_payload, hmac_secret, 'HS256'
526
+ token = JWT.encode(aud_payload, hmac_secret, 'HS256')
384
527
 
385
528
  begin
386
529
  # Add aud to the validation to check if the token has been manipulated
387
- decoded_token = JWT.decode token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' }
530
+ decoded_token = JWT.decode(token, hmac_secret, true, { aud: aud, verify_aud: true, algorithm: 'HS256' })
388
531
  rescue JWT::InvalidAudError
389
532
  # Handle invalid token, e.g. logout user or deny access
390
533
  puts 'Audience Error'
@@ -403,15 +546,15 @@ jti_raw = [hmac_secret, iat].join(':').to_s
403
546
  jti = Digest::MD5.hexdigest(jti_raw)
404
547
  jti_payload = { data: 'data', iat: iat, jti: jti }
405
548
 
406
- token = JWT.encode jti_payload, hmac_secret, 'HS256'
549
+ token = JWT.encode(jti_payload, hmac_secret, 'HS256')
407
550
 
408
551
  begin
409
552
  # If :verify_jti is true, validation will pass if a JTI is present
410
- #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
553
+ #decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' })
411
554
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
412
- decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
555
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' })
413
556
  # or
414
- decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
557
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' })
415
558
  rescue JWT::InvalidJtiError
416
559
  # Handle invalid token, e.g. logout user or deny access
417
560
  puts 'Error'
@@ -430,11 +573,11 @@ From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.or
430
573
  iat = Time.now.to_i
431
574
  iat_payload = { data: 'data', iat: iat }
432
575
 
433
- token = JWT.encode iat_payload, hmac_secret, 'HS256'
576
+ token = JWT.encode(iat_payload, hmac_secret, 'HS256')
434
577
 
435
578
  begin
436
579
  # Add iat to the validation to check if the token has been manipulated
437
- decoded_token = JWT.decode token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' }
580
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: true, algorithm: 'HS256' })
438
581
  rescue JWT::InvalidIatError
439
582
  # Handle invalid token, e.g. logout user or deny access
440
583
  end
@@ -450,16 +593,32 @@ From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/
450
593
  sub = 'Subject'
451
594
  sub_payload = { data: 'data', sub: sub }
452
595
 
453
- token = JWT.encode sub_payload, hmac_secret, 'HS256'
596
+ token = JWT.encode(sub_payload, hmac_secret, 'HS256')
454
597
 
455
598
  begin
456
599
  # Add sub to the validation to check if the token has been manipulated
457
- decoded_token = JWT.decode token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' }
600
+ decoded_token = JWT.decode(token, hmac_secret, true, { sub: sub, verify_sub: true, algorithm: 'HS256' })
458
601
  rescue JWT::InvalidSubError
459
602
  # Handle invalid token, e.g. logout user or deny access
460
603
  end
461
604
  ```
462
605
 
606
+ ### Standalone claim verification
607
+
608
+ The JWT claim verifications can be used to verify any Hash to include expected keys and values.
609
+
610
+ A few example on verifying the claims for a payload:
611
+ ```ruby
612
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :numeric, :exp)
613
+ JWT::Claims.valid_payload?({"exp" => Time.now.to_i + 10}, :exp)
614
+ # => true
615
+ JWT::Claims.payload_errors({"exp" => Time.now.to_i - 10}, :exp)
616
+ # => [#<struct JWT::Claims::Error message="Signature has expired">]
617
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
618
+
619
+ JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject")
620
+ ```
621
+
463
622
  ### Finding a Key
464
623
 
465
624
  To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
@@ -470,7 +629,7 @@ iss_payload = { data: 'data', iss: issuers.first }
470
629
 
471
630
  secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
472
631
 
473
- token = JWT.encode iss_payload, hmac_secret, 'HS256'
632
+ token = JWT.encode(iss_payload, hmac_secret, 'HS256')
474
633
 
475
634
  begin
476
635
  # Add iss to the validation to check if the token has been manipulated
@@ -486,28 +645,88 @@ end
486
645
 
487
646
  You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
488
647
  ```ruby
489
- # Will raise a JWT::ExpiredSignature error if the 'exp' claim is absent
490
- JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
648
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
649
+ JWT.decode(token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' })
491
650
  ```
492
651
 
493
- ### JSON Web Key (JWK)
652
+ ### X.509 certificates in x5c header
494
653
 
495
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
654
+ 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).
496
655
 
497
656
  ```ruby
498
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
499
- payload, headers = { data: 'data' }, { kid: jwk.kid }
657
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
658
+ crl_uris = root_certificates.map(&:crl_uris)
659
+ crls = crl_uris.map do |uri|
660
+ # look up cached CRL by `uri` and return it if found, otherwise continue
661
+ crl = Net::HTTP.get(uri)
662
+ crl = OpenSSL::X509::CRL.new(crl)
663
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
664
+ end
665
+
666
+ begin
667
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls } })
668
+ rescue JWT::DecodeError
669
+ # Handle error, e.g. x5c header certificate revoked or expired
670
+ end
671
+ ```
672
+
673
+ ## JSON Web Key (JWK)
500
674
 
501
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
675
+ JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
502
676
 
503
- # The jwk loader would fetch the set of JWKs from a trusted source
504
- jwk_loader = ->(options) do
505
- @cached_keys = nil if options[:invalidate] # need to reload the keys
506
- @cached_keys ||= { keys: [jwk.export] }
677
+ To encode a JWT using your JWK:
678
+
679
+ ```ruby
680
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
681
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
682
+
683
+ # Encoding
684
+ payload = { data: 'data' }
685
+ token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
686
+
687
+ # JSON Web Key Set for advertising your signing keys
688
+ jwks_hash = JWT::JWK::Set.new(jwk).export
689
+ ```
690
+
691
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
692
+
693
+ ```ruby
694
+ jwks = JWT::JWK::Set.new(jwks_hash)
695
+ jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
696
+ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
697
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
698
+ ```
699
+
700
+ The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
701
+ This can be used to implement caching of remotely fetched JWK Sets.
702
+
703
+ 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`.
704
+ The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
705
+
706
+ Tokens without a specified `kid` are rejected by default.
707
+ This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`.
708
+
709
+ ```ruby
710
+ jwks_loader = ->(options) do
711
+ # The jwk loader would fetch the set of JWKs from a trusted source.
712
+ # To avoid malicious requests triggering cache invalidations there needs to be
713
+ # some kind of grace time or other logic for determining the validity of the invalidation.
714
+ # This example only allows cache invalidations every 5 minutes.
715
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
716
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
717
+ @cached_keys = nil
718
+ end
719
+ @cached_keys ||= begin
720
+ @cache_last_update = Time.now.to_i
721
+ # Replace with your own JWKS fetching routine
722
+ jwks = JWT::JWK::Set.new(jwks_hash)
723
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
724
+ jwks
725
+ end
507
726
  end
508
727
 
509
728
  begin
510
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
729
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
511
730
  rescue JWT::JWKError
512
731
  # Handle problems with the provided JWKs
513
732
  rescue JWT::DecodeError
@@ -515,32 +734,56 @@ rescue JWT::DecodeError
515
734
  end
516
735
  ```
517
736
 
518
- or by passing JWK as a simple Hash
519
-
520
- ```
521
- jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
522
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
523
- ```
524
-
525
737
  ### Importing and exporting JSON Web Keys
526
738
 
527
- 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.
739
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
740
+ and export to either format with and without the private key included.
741
+
742
+ To include the private key in the export pass the `include_private` parameter to the export method.
528
743
 
529
744
  ```ruby
530
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
745
+ # Import a JWK Hash (showing an HMAC example)
746
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
531
747
 
748
+ # Import an OpenSSL key
749
+ # You can optionally add descriptive parameters to the JWK
750
+ desc_params = { kid: 'my-kid', use: 'sig' }
751
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
752
+
753
+ # Export as JWK Hash (public key only by default)
532
754
  jwk_hash = jwk.export
533
755
  jwk_hash_with_private_key = jwk.export(include_private: true)
756
+
757
+ # Export as OpenSSL key
758
+ public_key = jwk.verify_key
759
+ private_key = jwk.signing_key if jwk.private?
760
+
761
+ # You can also import and export entire JSON Web Key Sets
762
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
763
+ jwks = JWT::JWK::Set.new(jwks_hash)
764
+ jwks_hash = jwks.export
534
765
  ```
535
766
 
536
- # Development and Tests
767
+ ### Key ID (kid) and JWKs
537
768
 
538
- We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
769
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
770
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
771
+ or can be given to the JWK instance on initialization.
539
772
 
540
- ```bash
541
- rake release
773
+ ```ruby
774
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
775
+ # OR
776
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
777
+ # OR
778
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
779
+
780
+ jwk_hash = jwk.export
781
+
782
+ thumbprint_as_the_kid = jwk_hash[:kid]
542
783
  ```
543
784
 
785
+ # Development and testing
786
+
544
787
  The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
545
788
 
546
789
  ```bash
@@ -548,12 +791,23 @@ bundle install
548
791
  bundle exec appraisal rake test
549
792
  ```
550
793
 
551
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
794
+ # Releasing
795
+
796
+ To cut a new release adjust the [version.rb](lib/jwt/version.rb) and [CHANGELOG](CHANGELOG.md) with desired version numbers and dates and commit the changes. Tag the release with the version number using the following command:
797
+
798
+ ```bash
799
+ rake release:source_control_push
800
+ ```
801
+
802
+ This will tag a new version an trigger a [GitHub action](.github/workflows/push_gem.yml) that eventually will push the gem to rubygems.org.
803
+
804
+ ## How to contribute
805
+ See [CONTRIBUTING](CONTRIBUTING.md).
552
806
 
553
807
  ## Contributors
554
808
 
555
- See `AUTHORS` file.
809
+ See [AUTHORS](AUTHORS).
556
810
 
557
811
  ## License
558
812
 
559
- See `LICENSE` file.
813
+ See [LICENSE](LICENSE).