jwt 2.2.1 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +305 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +268 -40
  7. data/lib/jwt/base64.rb +16 -2
  8. data/lib/jwt/claims_validator.rb +13 -9
  9. data/lib/jwt/configuration/container.rb +32 -0
  10. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  11. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  12. data/lib/jwt/configuration.rb +15 -0
  13. data/lib/jwt/decode.rb +80 -18
  14. data/lib/jwt/deprecations.rb +29 -0
  15. data/lib/jwt/encode.rb +24 -19
  16. data/lib/jwt/error.rb +17 -14
  17. data/lib/jwt/jwa/ecdsa.rb +76 -0
  18. data/lib/jwt/jwa/eddsa.rb +42 -0
  19. data/lib/jwt/jwa/hmac.rb +75 -0
  20. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  21. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  22. data/lib/jwt/jwa/none.rb +19 -0
  23. data/lib/jwt/jwa/ps.rb +30 -0
  24. data/lib/jwt/jwa/rsa.rb +25 -0
  25. data/lib/jwt/{algos → jwa}/unsupported.rb +8 -5
  26. data/lib/jwt/jwa/wrapper.rb +26 -0
  27. data/lib/jwt/jwa.rb +62 -0
  28. data/lib/jwt/jwk/ec.rb +251 -0
  29. data/lib/jwt/jwk/hmac.rb +103 -0
  30. data/lib/jwt/jwk/key_base.rb +57 -0
  31. data/lib/jwt/jwk/key_finder.rb +19 -30
  32. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  33. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  34. data/lib/jwt/jwk/rsa.rb +181 -25
  35. data/lib/jwt/jwk/set.rb +80 -0
  36. data/lib/jwt/jwk/thumbprint.rb +26 -0
  37. data/lib/jwt/jwk.rb +39 -15
  38. data/lib/jwt/verify.rb +25 -6
  39. data/lib/jwt/version.rb +24 -3
  40. data/lib/jwt/x5c_key_finder.rb +52 -0
  41. data/lib/jwt.rb +6 -4
  42. data/ruby-jwt.gemspec +18 -10
  43. metadata +45 -76
  44. data/.codeclimate.yml +0 -20
  45. data/.ebert.yml +0 -18
  46. data/.gitignore +0 -11
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -98
  49. data/.travis.yml +0 -20
  50. data/Appraisals +0 -14
  51. data/Gemfile +0 -3
  52. data/Rakefile +0 -11
  53. data/lib/jwt/algos/ecdsa.rb +0 -35
  54. data/lib/jwt/algos/eddsa.rb +0 -23
  55. data/lib/jwt/algos/hmac.rb +0 -33
  56. data/lib/jwt/algos/ps.rb +0 -43
  57. data/lib/jwt/algos/rsa.rb +0 -19
  58. data/lib/jwt/default_options.rb +0 -15
  59. data/lib/jwt/security_utils.rb +0 -57
  60. 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,28 @@ 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
+ ### Deprecation warnings
47
+
48
+ 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.
49
+
50
+ ```ruby
51
+ JWT.configuration.deprecation_warnings = :warn # default is :once
52
+ ```
53
+
54
+ ### Base64 decoding
55
+
56
+ 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).
57
+
58
+ The stricter base64 decoding when processing tokens can be done via the `strict_base64_decoding` configuration accessor.
59
+ ```ruby
60
+ JWT.configuration.strict_base64_decoding = true # default is false
61
+ ```
62
+
63
+ ### **NONE**
40
64
 
41
65
  * none - unsigned token
42
66
 
@@ -62,14 +86,14 @@ decoded_token = JWT.decode token, nil, false
62
86
  puts decoded_token
63
87
  ```
64
88
 
65
- **HMAC**
89
+ ### **HMAC**
66
90
 
67
91
  * HS256 - HMAC using SHA-256 hash algorithm
68
- * HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
69
92
  * HS384 - HMAC using SHA-384 hash algorithm
70
93
  * HS512 - HMAC using SHA-512 hash algorithm
71
94
 
72
95
  ```ruby
96
+ # 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
97
  hmac_secret = 'my$ecretK3y'
74
98
 
75
99
  token = JWT.encode payload, hmac_secret, 'HS256'
@@ -87,13 +111,7 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
87
111
  puts decoded_token
88
112
  ```
89
113
 
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.
91
-
92
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
93
- [libsodium](https://github.com/jedisct1/libsodium), it can be installed
94
- on MacOS with `brew install libsodium`.
95
-
96
- **RSA**
114
+ ### **RSA**
97
115
 
98
116
  * RS256 - RSA using SHA-256 hash algorithm
99
117
  * RS384 - RSA using SHA-384 hash algorithm
@@ -118,24 +136,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
118
136
  puts decoded_token
119
137
  ```
120
138
 
121
- **ECDSA**
139
+ ### **ECDSA**
122
140
 
123
141
  * ES256 - ECDSA using P-256 and SHA-256
124
142
  * ES384 - ECDSA using P-384 and SHA-384
125
143
  * ES512 - ECDSA using P-521 and SHA-512
144
+ * ES256K - ECDSA using P-256K and SHA-256
126
145
 
127
146
  ```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
147
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
132
148
 
133
149
  token = JWT.encode payload, ecdsa_key, 'ES256'
134
150
 
135
151
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
136
152
  puts token
137
153
 
138
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
154
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
139
155
 
140
156
  # Array
141
157
  # [
@@ -145,7 +161,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
145
161
  puts decoded_token
146
162
  ```
147
163
 
148
- **EDDSA**
164
+ ### **EDDSA**
149
165
 
150
166
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
151
167
 
@@ -153,7 +169,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
153
169
  gem 'rbnacl'
154
170
  ```
155
171
 
156
- For more detailed installation instruction check the official [repository](https://github.com/cryptosphere/rbnacl) on GitHub.
172
+ For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
157
173
 
158
174
  * ED25519
159
175
 
@@ -174,9 +190,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
174
190
 
175
191
  ```
176
192
 
177
- **RSASSA-PSS**
193
+ ### **RSASSA-PSS**
178
194
 
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`.
195
+ 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
196
 
181
197
  ```ruby
182
198
  gem 'openssl', '~> 2.1'
@@ -205,6 +221,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
205
221
  puts decoded_token
206
222
  ```
207
223
 
224
+ ### **Custom algorithms**
225
+
226
+ 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.
227
+
228
+ ```ruby
229
+ module CustomHS512Algorithm
230
+ def self.alg
231
+ 'HS512'
232
+ end
233
+
234
+ def self.valid_alg?(alg_to_validate)
235
+ alg_to_validate == alg
236
+ end
237
+
238
+ def self.sign(data:, signing_key:)
239
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), data, signing_key)
240
+ end
241
+
242
+ def self.verify(data:, signature:, verification_key:)
243
+ ::OpenSSL.secure_compare(sign(data: data, signing_key: verification_key), signature)
244
+ end
245
+ end
246
+
247
+ token = ::JWT.encode({'pay' => 'load'}, 'secret', CustomHS512Algorithm)
248
+ payload, header = ::JWT.decode(token, 'secret', true, algorithm: CustomHS512Algorithm)
249
+ ```
250
+
208
251
  ## Support for reserved claim names
209
252
  JSON Web Token defines some reserved claim names and defines how they should be
210
253
  used. JWT supports these reserved claim names:
@@ -270,6 +313,12 @@ rescue JWT::ExpiredSignature
270
313
  end
271
314
  ```
272
315
 
316
+ The Expiration Claim verification can be disabled.
317
+ ```ruby
318
+ # Decode token without raising JWT::ExpiredSignature error
319
+ JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
320
+ ```
321
+
273
322
  **Adding Leeway**
274
323
 
275
324
  ```ruby
@@ -310,6 +359,12 @@ rescue JWT::ImmatureSignature
310
359
  end
311
360
  ```
312
361
 
362
+ The Not Before Claim verification can be disabled.
363
+ ```ruby
364
+ # Decode token without raising JWT::ImmatureSignature error
365
+ JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
366
+ ```
367
+
313
368
  **Adding Leeway**
314
369
 
315
370
  ```ruby
@@ -351,6 +406,36 @@ rescue JWT::InvalidIssuerError
351
406
  end
352
407
  ```
353
408
 
409
+ You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
410
+ On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
411
+ to convert them to proc (using `to_proc`)
412
+
413
+ ```ruby
414
+ JWT.decode token, hmac_secret, true,
415
+ iss: %r'https://my.awesome.website/',
416
+ verify_iss: true,
417
+ algorithm: 'HS256'
418
+ ```
419
+
420
+ ```ruby
421
+ JWT.decode token, hmac_secret, true,
422
+ iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
423
+ verify_iss: true,
424
+ algorithm: 'HS256'
425
+ ```
426
+
427
+ ```ruby
428
+ JWT.decode token, hmac_secret, true,
429
+ iss: method(:valid_issuer?),
430
+ verify_iss: true,
431
+ algorithm: 'HS256'
432
+
433
+ # somewhere in the same class:
434
+ def valid_issuer?(issuer)
435
+ # custom validation
436
+ end
437
+ ```
438
+
354
439
  ### Audience Claim
355
440
 
356
441
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -391,6 +476,8 @@ begin
391
476
  #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
392
477
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
393
478
  decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
479
+ # or
480
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
394
481
  rescue JWT::InvalidJtiError
395
482
  # Handle invalid token, e.g. logout user or deny access
396
483
  puts 'Error'
@@ -439,31 +526,170 @@ rescue JWT::InvalidSubError
439
526
  end
440
527
  ```
441
528
 
529
+ ### Finding a Key
530
+
531
+ 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.
532
+
533
+ ```ruby
534
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
535
+ iss_payload = { data: 'data', iss: issuers.first }
536
+
537
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
538
+
539
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
540
+
541
+ begin
542
+ # Add iss to the validation to check if the token has been manipulated
543
+ decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
544
+ secrets[payload['iss']]
545
+ end
546
+ rescue JWT::InvalidIssuerError
547
+ # Handle invalid token, e.g. logout user or deny access
548
+ end
549
+ ```
550
+
551
+ ### Required Claims
552
+
553
+ You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
554
+ ```ruby
555
+ # Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
556
+ JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
557
+ ```
558
+
559
+ ### X.509 certificates in x5c header
560
+
561
+ 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).
562
+
563
+ ```ruby
564
+ root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
565
+ crl_uris = root_certificates.map(&:crl_uris)
566
+ crls = crl_uris.map do |uri|
567
+ # look up cached CRL by `uri` and return it if found, otherwise continue
568
+ crl = Net::HTTP.get(uri)
569
+ crl = OpenSSL::X509::CRL.new(crl)
570
+ # cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
571
+ end
572
+
573
+ begin
574
+ JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls } })
575
+ rescue JWT::DecodeError
576
+ # Handle error, e.g. x5c header certificate revoked or expired
577
+ end
578
+ ```
579
+
442
580
  ### JSON Web Key (JWK)
443
581
 
444
- JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
582
+ 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.
583
+
584
+ To encode a JWT using your JWK:
585
+
586
+ ```ruby
587
+ optional_parameters = { kid: 'my-kid', use: 'sig', alg: 'RS512' }
588
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
589
+
590
+ # Encoding
591
+ payload = { data: 'data' }
592
+ token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
593
+
594
+ # JSON Web Key Set for advertising your signing keys
595
+ jwks_hash = JWT::JWK::Set.new(jwk).export
596
+ ```
597
+
598
+ To decode a JWT using a trusted entity's JSON Web Key Set (JWKS):
445
599
 
446
600
  ```ruby
447
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
448
- payload, headers = { data: 'data' }, { kid: jwk.kid }
601
+ jwks = JWT::JWK::Set.new(jwks_hash)
602
+ jwks.filter! {|key| key[:use] == 'sig' } # Signing keys only!
603
+ algorithms = jwks.map { |key| key[:alg] }.compact.uniq
604
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwks)
605
+ ```
449
606
 
450
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
451
607
 
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] }
608
+ The `jwks` option can also be given as a lambda that evaluates every time a kid is resolved.
609
+ This can be used to implement caching of remotely fetched JWK Sets.
610
+
611
+ 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`.
612
+ The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
613
+
614
+ Tokens without a specified `kid` are rejected by default.
615
+ This behaviour may be overwritten by setting the `allow_nil_kid` option for `decode` to `true`.
616
+
617
+ ```ruby
618
+ jwks_loader = ->(options) do
619
+ # The jwk loader would fetch the set of JWKs from a trusted source.
620
+ # To avoid malicious requests triggering cache invalidations there needs to be
621
+ # some kind of grace time or other logic for determining the validity of the invalidation.
622
+ # This example only allows cache invalidations every 5 minutes.
623
+ if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300
624
+ logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache")
625
+ @cached_keys = nil
626
+ end
627
+ @cached_keys ||= begin
628
+ @cache_last_update = Time.now.to_i
629
+ # Replace with your own JWKS fetching routine
630
+ jwks = JWT::JWK::Set.new(jwks_hash)
631
+ jwks.select! { |key| key[:use] == 'sig' } # Signing Keys only
632
+ jwks
633
+ end
456
634
  end
457
635
 
458
636
  begin
459
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
637
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
460
638
  rescue JWT::JWKError
461
639
  # Handle problems with the provided JWKs
462
640
  rescue JWT::DecodeError
463
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
641
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
464
642
  end
465
643
  ```
466
644
 
645
+ ### Importing and exporting JSON Web Keys
646
+
647
+ The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys
648
+ and export to either format with and without the private key included.
649
+
650
+ To include the private key in the export pass the `include_private` parameter to the export method.
651
+
652
+ ```ruby
653
+ # Import a JWK Hash (showing an HMAC example)
654
+ jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' })
655
+
656
+ # Import an OpenSSL key
657
+ # You can optionally add descriptive parameters to the JWK
658
+ desc_params = { kid: 'my-kid', use: 'sig' }
659
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params)
660
+
661
+ # Export as JWK Hash (public key only by default)
662
+ jwk_hash = jwk.export
663
+ jwk_hash_with_private_key = jwk.export(include_private: true)
664
+
665
+ # Export as OpenSSL key
666
+ public_key = jwk.verify_key
667
+ private_key = jwk.signing_key if jwk.private?
668
+
669
+ # You can also import and export entire JSON Web Key Sets
670
+ jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
671
+ jwks = JWT::JWK::Set.new(jwks_hash)
672
+ jwks_hash = jwks.export
673
+ ```
674
+
675
+ ### Key ID (kid) and JWKs
676
+
677
+ The key id (kid) generation in the gem is a custom algorithm and not based on any standards.
678
+ To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration
679
+ or can be given to the JWK instance on initialization.
680
+
681
+ ```ruby
682
+ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint
683
+ # OR
684
+ JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint
685
+ # OR
686
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint)
687
+
688
+ jwk_hash = jwk.export
689
+
690
+ thumbprint_as_the_kid = jwk_hash[:kid]
691
+ ```
692
+
467
693
  # Development and Tests
468
694
 
469
695
  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 +698,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
472
698
  rake release
473
699
  ```
474
700
 
475
- The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with
701
+ 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
702
 
477
703
  ```bash
478
- bundle exec rspec
704
+ bundle install
705
+ bundle exec appraisal rake test
479
706
  ```
480
707
 
481
- **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
708
+ ## How to contribute
709
+ See [CONTRIBUTING](CONTRIBUTING.md).
482
710
 
483
711
  ## Contributors
484
712
 
485
- See `AUTHORS` file.
713
+ See [AUTHORS](AUTHORS).
486
714
 
487
715
  ## License
488
716
 
489
- See `LICENSE` file.
717
+ See [LICENSE](LICENSE).
data/lib/jwt/base64.rb CHANGED
@@ -3,14 +3,28 @@
3
3
  require 'base64'
4
4
 
5
5
  module JWT
6
- # Base64 helpers
6
+ # Base64 encoding and decoding
7
7
  class Base64
8
8
  class << self
9
+ # Encode a string with URL-safe Base64 complying with RFC 4648 (not padded).
9
10
  def url_encode(str)
10
- ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
11
+ ::Base64.urlsafe_encode64(str, padding: false)
11
12
  end
12
13
 
14
+ # Decode a string with URL-safe Base64 complying with RFC 4648.
15
+ # Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software")
13
16
  def url_decode(str)
17
+ ::Base64.urlsafe_decode64(str)
18
+ rescue ArgumentError => e
19
+ raise unless e.message == 'invalid base64'
20
+ raise Base64DecodeError, 'Invalid base64 encoding' if JWT.configuration.strict_base64_decoding
21
+
22
+ loose_urlsafe_decode64(str).tap do
23
+ Deprecations.warning('Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt')
24
+ end
25
+ end
26
+
27
+ def loose_urlsafe_decode64(str)
14
28
  str += '=' * (4 - str.length.modulo(4))
15
29
  ::Base64.decode64(str.tr('-_', '+/'))
16
30
  end