jwt 2.2.1 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +271 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +253 -35
  7. data/lib/jwt/algos/algo_wrapper.rb +26 -0
  8. data/lib/jwt/algos/ecdsa.rb +55 -14
  9. data/lib/jwt/algos/eddsa.rb +18 -8
  10. data/lib/jwt/algos/hmac.rb +57 -17
  11. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  12. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  13. data/lib/jwt/algos/none.rb +19 -0
  14. data/lib/jwt/algos/ps.rb +10 -12
  15. data/lib/jwt/algos/rsa.rb +9 -5
  16. data/lib/jwt/algos/unsupported.rb +7 -4
  17. data/lib/jwt/algos.rb +66 -0
  18. data/lib/jwt/claims_validator.rb +12 -8
  19. data/lib/jwt/configuration/container.rb +21 -0
  20. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  21. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  22. data/lib/jwt/configuration.rb +15 -0
  23. data/lib/jwt/decode.rb +85 -17
  24. data/lib/jwt/encode.rb +30 -19
  25. data/lib/jwt/error.rb +16 -14
  26. data/lib/jwt/jwk/ec.rb +236 -0
  27. data/lib/jwt/jwk/hmac.rb +103 -0
  28. data/lib/jwt/jwk/key_base.rb +55 -0
  29. data/lib/jwt/jwk/key_finder.rb +19 -30
  30. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  31. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  32. data/lib/jwt/jwk/rsa.rb +181 -25
  33. data/lib/jwt/jwk/set.rb +80 -0
  34. data/lib/jwt/jwk/thumbprint.rb +26 -0
  35. data/lib/jwt/jwk.rb +39 -15
  36. data/lib/jwt/verify.rb +18 -3
  37. data/lib/jwt/version.rb +23 -3
  38. data/lib/jwt/x5c_key_finder.rb +55 -0
  39. data/lib/jwt.rb +5 -4
  40. data/ruby-jwt.gemspec +15 -10
  41. metadata +30 -90
  42. data/.codeclimate.yml +0 -20
  43. data/.ebert.yml +0 -18
  44. data/.gitignore +0 -11
  45. data/.rspec +0 -1
  46. data/.rubocop.yml +0 -98
  47. data/.travis.yml +0 -20
  48. data/Appraisals +0 -14
  49. data/Gemfile +0 -3
  50. data/Rakefile +0 -11
  51. data/lib/jwt/default_options.rb +0 -15
  52. data/lib/jwt/security_utils.rb +0 -57
  53. data/lib/jwt/signature.rb +0 -52
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at antmanj@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,99 @@
1
+ # Contributing to [ruby-jwt](https://github.com/jwt/ruby-jwt)
2
+
3
+ ## Forking the project
4
+
5
+ Fork the project on GitHub and clone your own fork. Instuctions on forking can be found from the [GitHub Docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
6
+
7
+ ```
8
+ git clone git@github.com:you/ruby-jwt.git
9
+ cd ruby-jwt
10
+ git remote add upstream https://github.com/jwt/ruby-jwt
11
+ ```
12
+
13
+ ## Create a branch for your implementation
14
+
15
+ Make sure you have the latest upstream main branch of the project.
16
+
17
+ ```
18
+ git fetch --all
19
+ git checkout main
20
+ git rebase upstream/main
21
+ git push origin main
22
+ git checkout -b fix-a-little-problem
23
+ ```
24
+
25
+ ## Running the tests and linter
26
+
27
+ Before you start with your implementation make sure you are able to get a successful test run with the current revision.
28
+
29
+ The tests are written with rspec and [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
30
+
31
+ [Rubocop](https://github.com/rubocop/rubocop) is used to enforce the Ruby style.
32
+
33
+ To run the complete set of tests and linter run the following
34
+
35
+ ```bash
36
+ bundle install
37
+ bundle exec appraisal rake test
38
+ bundle exec rubocop
39
+ ```
40
+
41
+ ## Implement your feature
42
+
43
+ Implement tests and your change. Don't be shy adding a little something in the [README](README.md).
44
+ Add a short description of the change in either the `Features` or `Fixes` section in the [CHANGELOG](CHANGELOG.md) file.
45
+
46
+ The form of the row (You need to return to the row when you know the pull request id)
47
+ ```
48
+ - Fix a little problem [#123](https://github.com/jwt/ruby-jwt/pull/123) - [@you](https://github.com/you).
49
+ ```
50
+
51
+ ## Push your branch and create a pull request
52
+
53
+ Before pushing make sure the tests pass and RuboCop is happy.
54
+
55
+ ```
56
+ bundle exec appraisal rake test
57
+ bundle exec rubocop
58
+ git push origin fix-a-little-problem
59
+ ```
60
+
61
+ Make a new pull request on the [ruby-jwt project](https://github.com/jwt/ruby-jwt/pulls) with a description what the change is about.
62
+
63
+ ## Update the CHANGELOG, again
64
+
65
+ Update the [CHANGELOG](CHANGELOG.md) with the pull request id from the previous step.
66
+
67
+ You can ammend the previous commit with the updated changelog change and force push your branch. The PR will get automatically updated.
68
+
69
+ ```
70
+ git add CHANGELOG.md
71
+ git commit --amend --no-edit
72
+ git push origin fix-a-little-problem -f
73
+ ```
74
+
75
+ ## Keep an eye on your pull request
76
+
77
+ A maintainer will review and probably merge you changes when time allows, be patient.
78
+
79
+ ## Keeping your branch up-to-date
80
+
81
+ It's recommended that you keep your branch up-to-date by rebasing to the upstream main.
82
+
83
+ ```
84
+ git fetch upstream
85
+ git checkout fix-a-little-problem
86
+ git rebase upstream/main
87
+ git push origin fix-a-little-problem -f
88
+ ```
89
+
90
+ # Releasing a new version
91
+
92
+ The version is using the [Semantic Versioning](http://semver.org/) and the version is located in the [version.rb](lib/jwt/version.rb) file.
93
+ Also update the [CHANGELOG](CHANGELOG.md) to reflect the upcoming version release.
94
+
95
+ ```bash
96
+ rake release
97
+ ```
98
+
99
+ **If you want a release cut with your PR, please include a version bump according to **
data/README.md CHANGED
@@ -1,26 +1,33 @@
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=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
- [![Ebert](https://ebertapp.io/github/jwt/ruby-jwt.svg)](https://ebertapp.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
13
  ## Announcements
15
-
14
+ * Ruby 2.4 support was dropped in version 2.4.0
16
15
  * Ruby 1.9.3 support was dropped at December 31st, 2016.
17
16
  * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
18
17
 
18
+ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes.
19
+
20
+ ## Sponsors
21
+
22
+ |Logo|Message|
23
+ |-|-|
24
+ |![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png)|If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth)|
25
+
19
26
  ## Installing
20
27
 
21
28
  ### Using Rubygems:
22
29
  ```bash
23
- sudo gem install jwt
30
+ gem install jwt
24
31
  ```
25
32
 
26
33
  ### Using Bundler:
@@ -32,11 +39,11 @@ And run `bundle install`
32
39
 
33
40
  ## Algorithms and Usage
34
41
 
35
- 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/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
42
+ The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
36
43
 
37
44
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
38
45
 
39
- **NONE**
46
+ ### **NONE**
40
47
 
41
48
  * none - unsigned token
42
49
 
@@ -62,7 +69,7 @@ decoded_token = JWT.decode token, nil, false
62
69
  puts decoded_token
63
70
  ```
64
71
 
65
- **HMAC**
72
+ ### **HMAC**
66
73
 
67
74
  * HS256 - HMAC using SHA-256 hash algorithm
68
75
  * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
@@ -70,6 +77,7 @@ puts decoded_token
70
77
  * HS512 - HMAC using SHA-512 hash algorithm
71
78
 
72
79
  ```ruby
80
+ # The secret must be a string. With OpenSSL 3.0/openssl gem `<3.0.1`, JWT::DecodeError will be raised if it isn't provided.
73
81
  hmac_secret = 'my$ecretK3y'
74
82
 
75
83
  token = JWT.encode payload, hmac_secret, 'HS256'
@@ -87,13 +95,13 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
87
95
  puts decoded_token
88
96
  ```
89
97
 
90
- 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
+ Note: If [RbNaCl](https://github.com/RubyCrypto/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512-256, and HMAC-SHA512. RbNaCl prior to 6.0.0 only support a maximum key size of 32 bytes for these algorithms.
91
99
 
92
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
100
+ [RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
93
101
  [libsodium](https://github.com/jedisct1/libsodium), it can be installed
94
102
  on MacOS with `brew install libsodium`.
95
103
 
96
- **RSA**
104
+ ### **RSA**
97
105
 
98
106
  * RS256 - RSA using SHA-256 hash algorithm
99
107
  * RS384 - RSA using SHA-384 hash algorithm
@@ -118,24 +126,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
118
126
  puts decoded_token
119
127
  ```
120
128
 
121
- **ECDSA**
129
+ ### **ECDSA**
122
130
 
123
131
  * ES256 - ECDSA using P-256 and SHA-256
124
132
  * ES384 - ECDSA using P-384 and SHA-384
125
133
  * ES512 - ECDSA using P-521 and SHA-512
134
+ * ES256K - ECDSA using P-256K and SHA-256
126
135
 
127
136
  ```ruby
128
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
129
- ecdsa_key.generate_key
130
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
131
- ecdsa_public.private_key = nil
137
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
132
138
 
133
139
  token = JWT.encode payload, ecdsa_key, 'ES256'
134
140
 
135
141
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
136
142
  puts token
137
143
 
138
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
144
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
139
145
 
140
146
  # Array
141
147
  # [
@@ -145,7 +151,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
145
151
  puts decoded_token
146
152
  ```
147
153
 
148
- **EDDSA**
154
+ ### **EDDSA**
149
155
 
150
156
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
151
157
 
@@ -153,7 +159,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
153
159
  gem 'rbnacl'
154
160
  ```
155
161
 
156
- For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
162
+ For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
157
163
 
158
164
  * ED25519
159
165
 
@@ -174,9 +180,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
174
180
 
175
181
  ```
176
182
 
177
- **RSASSA-PSS**
183
+ ### **RSASSA-PSS**
178
184
 
179
- 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`.
185
+ In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`.
180
186
 
181
187
  ```ruby
182
188
  gem 'openssl', '~> 2.1'
@@ -205,6 +211,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
205
211
  puts decoded_token
206
212
  ```
207
213
 
214
+ ### **Custom algorithms**
215
+
216
+ An object implementing custom signing or verification behaviour can be passed in the `algorithm` option when encoding and decoding. The given object needs to implement the method `valid_alg?` and `verify` and/or `alg` and `sign`, depending if object is used for encoding or decoding.
217
+
218
+ ```ruby
219
+ module CustomHS512Algorithm
220
+ def self.alg
221
+ 'HS512'
222
+ end
223
+
224
+ def self.valid_alg?(alg_to_validate)
225
+ alg_to_validate == alg
226
+ end
227
+
228
+ def self.sign(data:, signing_key:)
229
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
230
+ end
231
+
232
+ def self.verify(data:, signature:, verification_key:)
233
+ ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature)
234
+ end
235
+ end
236
+
237
+ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
238
+ payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
239
+ ```
240
+
208
241
  ## Support for reserved claim names
209
242
  JSON Web Token defines some reserved claim names and defines how they should be
210
243
  used. JWT supports these reserved claim names:
@@ -270,6 +303,12 @@ rescue JWT::ExpiredSignature
270
303
  end
271
304
  ```
272
305
 
306
+ The Expiration Claim verification can be disabled.
307
+ ```ruby
308
+ # Decode token without raising JWT::ExpiredSignature error
309
+ JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
310
+ ```
311
+
273
312
  **Adding Leeway**
274
313
 
275
314
  ```ruby
@@ -310,6 +349,12 @@ rescue JWT::ImmatureSignature
310
349
  end
311
350
  ```
312
351
 
352
+ The Not Before Claim verification can be disabled.
353
+ ```ruby
354
+ # Decode token without raising JWT::ImmatureSignature error
355
+ JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
356
+ ```
357
+
313
358
  **Adding Leeway**
314
359
 
315
360
  ```ruby
@@ -351,6 +396,36 @@ rescue JWT::InvalidIssuerError
351
396
  end
352
397
  ```
353
398
 
399
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
400
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
401
+ to convert them to proc (using `to_proc`)
402
+
403
+ ```ruby
404
+ JWT.decode token, hmac_secret, true,
405
+ iss: %r'https://my.awesome.website/',
406
+ verify_iss: true,
407
+ algorithm: 'HS256'
408
+ ```
409
+
410
+ ```ruby
411
+ JWT.decode token, hmac_secret, true,
412
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
413
+ verify_iss: true,
414
+ algorithm: 'HS256'
415
+ ```
416
+
417
+ ```ruby
418
+ JWT.decode token, hmac_secret, true,
419
+ iss: method(:valid_issuer?),
420
+ verify_iss: true,
421
+ algorithm: 'HS256'
422
+
423
+ # somewhere in the same class:
424
+ def valid_issuer?(issuer)
425
+ # custom validation
426
+ end
427
+ ```
428
+
354
429
  ### Audience Claim
355
430
 
356
431
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -391,6 +466,8 @@ begin
391
466
  #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
392
467
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
393
468
  decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
469
+ # or
470
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
394
471
  rescue JWT::InvalidJtiError
395
472
  # Handle invalid token, e.g. logout user or deny access
396
473
  puts 'Error'
@@ -439,31 +516,170 @@ rescue JWT::InvalidSubError
439
516
  end
440
517
  ```
441
518
 
519
+ ### Finding a Key
520
+
521
+ To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
522
+
523
+ ```ruby
524
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
525
+ iss_payload = { data: 'data', iss: issuers.first }
526
+
527
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
528
+
529
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
530
+
531
+ begin
532
+ # Add iss to the validation to check if the token has been manipulated
533
+ decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
534
+ secrets[payload['iss']]
535
+ end
536
+ rescue JWT::InvalidIssuerError
537
+ # Handle invalid token, e.g. logout user or deny access
538
+ end
539
+ ```
540
+
541
+ ### Required Claims
542
+
543
+ You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
544
+ ```ruby
545
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
546
+ JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
547
+ ```
548
+
549
+ ### X.509 certificates in x5c header
550
+
551
+ A JWT signature can be verified using certificate(s) given in the `x5c` header. Before doing that, the trustworthiness of these certificate(s) must be established. This is done in accordance with RFC 5280 which (among other things) verifies the certificate(s) are issued by a trusted root certificate, the timestamps are valid, and none of the certificate(s) are revoked (i.e. being present in the root certificate's Certificate Revocation List).
552
+
553
+ ```ruby
554
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
555
+ crl_uris = root_certificates.map(&:crl_uris)
556
+ crls = crl_uris.map do |uri|
557
+ # look up cached CRL by `uri` and return it if found, otherwise continue
558
+ crl = Net::HTTP.get(uri)
559
+ crl = OpenSSL::X509::CRL.new(crl)
560
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
561
+ end
562
+
563
+ begin
564
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
565
+ rescue JWT::DecodeError
566
+ # Handle error, e.g. x5c header certificate revoked or expired
567
+ end
568
+ ```
569
+
442
570
  ### JSON Web Key (JWK)
443
571
 
444
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
572
+ JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
573
+
574
+ To encode a JWT using your JWK:
575
+
576
+ ```ruby
577
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
578
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
579
+
580
+ # Encoding
581
+ payload = { data: 'data' }
582
+ token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
583
+
584
+ # JSON Web Key Set for advertising your signing keys
585
+ jwks_hash = JWT::JWK::Set.new(jwk).export
586
+ ```
587
+
588
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
445
589
 
446
590
  ```ruby
447
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
448
- payload, headers = { data: 'data' }, { kid: jwk.kid }
591
+ jwks = JWT::JWK::Set.new(jwks_hash)
592
+ jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
593
+ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
594
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
595
+ ```
596
+
597
+
598
+ The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
599
+ This can be used to implement caching of remotely fetched JWK Sets.
449
600
 
450
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
601
+ If the requested `kid` is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`.
602
+ The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
451
603
 
452
- # The jwk loader would fetch the set of JWKs from a trusted source
453
- jwk_loader = ->(options) do
454
- @cached_keys = nil if options[:invalidate] # need to reload the keys
455
- @cached_keys ||= { keys: [jwk.export] }
604
+ Tokens without a specified `kid` are rejected by default.
605
+ This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`.
606
+
607
+ ```ruby
608
+ jwks_loader = ->(options) do
609
+ # The jwk loader would fetch the set of JWKs from a trusted source.
610
+ # To avoid malicious requests triggering cache invalidations there needs to be
611
+ # some kind of grace time or other logic for determining the validity of the invalidation.
612
+ # This example only allows cache invalidations every 5 minutes.
613
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
614
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
615
+ @cached_keys = nil
616
+ end
617
+ @cached_keys ||= begin
618
+ @cache_last_update = Time.now.to_i
619
+ # Replace with your own JWKS fetching routine
620
+ jwks = JWT::JWK::Set.new(jwks_hash)
621
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
622
+ jwks
623
+ end
456
624
  end
457
625
 
458
626
  begin
459
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
627
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
460
628
  rescue JWT::JWKError
461
629
  # Handle problems with the provided JWKs
462
630
  rescue JWT::DecodeError
463
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
631
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
464
632
  end
465
633
  ```
466
634
 
635
+ ### Importing and exporting JSON Web Keys
636
+
637
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
638
+ and export to either format with and without the private key included.
639
+
640
+ To include the private key in the export pass the `include_private` parameter to the export method.
641
+
642
+ ```ruby
643
+ # Import a JWK Hash (showing an HMAC example)
644
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
645
+
646
+ # Import an OpenSSL key
647
+ # You can optionally add descriptive parameters to the JWK
648
+ desc_params = { kid: 'my-kid', use: 'sig' }
649
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
650
+
651
+ # Export as JWK Hash (public key only by default)
652
+ jwk_hash = jwk.export
653
+ jwk_hash_with_private_key = jwk.export(include_private: true)
654
+
655
+ # Export as OpenSSL key
656
+ public_key = jwk.verify_key
657
+ private_key = jwk.signing_key if jwk.private?
658
+
659
+ # You can also import and export entire JSON Web Key Sets
660
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
661
+ jwks = JWT::JWK::Set.new(jwks_hash)
662
+ jwks_hash = jwks.export
663
+ ```
664
+
665
+ ### Key ID (kid) and JWKs
666
+
667
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
668
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
669
+ or can be given to the JWK instance on initialization.
670
+
671
+ ```ruby
672
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
673
+ # OR
674
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
675
+ # OR
676
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
677
+
678
+ jwk_hash = jwk.export
679
+
680
+ thumbprint_as_the_kid = jwk_hash[:kid]
681
+ ```
682
+
467
683
  # Development and Tests
468
684
 
469
685
  We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
@@ -472,18 +688,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
472
688
  rake release
473
689
  ```
474
690
 
475
- The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with
691
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
476
692
 
477
693
  ```bash
478
- bundle exec rspec
694
+ bundle install
695
+ bundle exec appraisal rake test
479
696
  ```
480
697
 
481
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
698
+ ## How to contribute
699
+ See [CONTRIBUTING](CONTRIBUTING.md).
482
700
 
483
701
  ## Contributors
484
702
 
485
- See `AUTHORS` file.
703
+ See [AUTHORS](AUTHORS).
486
704
 
487
705
  ## License
488
706
 
489
- See `LICENSE` file.
707
+ See [LICENSE](LICENSE).
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ class AlgoWrapper
6
+ attr_reader :alg, :cls
7
+
8
+ def initialize(alg, cls)
9
+ @alg = alg
10
+ @cls = cls
11
+ end
12
+
13
+ def valid_alg?(alg_to_check)
14
+ alg&.casecmp(alg_to_check)&.zero? == true
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ cls.sign(alg, data, signing_key)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ cls.verify(alg, verification_key, data, signature)
23
+ end
24
+ end
25
+ end
26
+ end