jwt 2.2.2 → 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 +177 -5
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +252 -49
  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 +56 -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 +84 -23
  24. data/lib/jwt/encode.rb +30 -19
  25. data/lib/jwt/error.rb +2 -0
  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 +175 -26
  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 +16 -11
  41. metadata +27 -87
  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 -29
  48. data/Appraisals +0 -18
  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 -54
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'
@@ -79,21 +87,6 @@ puts token
79
87
 
80
88
  decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
81
89
 
82
- # Array
83
- # [
84
- # {"data"=>"test"}, # payload
85
- # {"alg"=>"HS256"} # header
86
- # ]
87
- puts decoded_token
88
-
89
- # Without secret key
90
- token = JWT.encode payload, nil, 'HS256'
91
-
92
- # eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pVzcY2dX8JNM3LzIYeP2B1e1Wcpt1K3TWVvIYSF4x-o
93
- puts token
94
-
95
- decoded_token = JWT.decode token, nil, true, { algorithm: 'HS256' }
96
-
97
90
  # Array
98
91
  # [
99
92
  # {"data"=>"test"}, # payload
@@ -102,13 +95,13 @@ decoded_token = JWT.decode token, nil, true, { algorithm: 'HS256' }
102
95
  puts decoded_token
103
96
  ```
104
97
 
105
- 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.
106
99
 
107
- [RbNaCl](https://github.com/cryptosphere/rbnacl) requires
100
+ [RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
108
101
  [libsodium](https://github.com/jedisct1/libsodium), it can be installed
109
102
  on MacOS with `brew install libsodium`.
110
103
 
111
- **RSA**
104
+ ### **RSA**
112
105
 
113
106
  * RS256 - RSA using SHA-256 hash algorithm
114
107
  * RS384 - RSA using SHA-384 hash algorithm
@@ -133,24 +126,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
133
126
  puts decoded_token
134
127
  ```
135
128
 
136
- **ECDSA**
129
+ ### **ECDSA**
137
130
 
138
131
  * ES256 - ECDSA using P-256 and SHA-256
139
132
  * ES384 - ECDSA using P-384 and SHA-384
140
133
  * ES512 - ECDSA using P-521 and SHA-512
134
+ * ES256K - ECDSA using P-256K and SHA-256
141
135
 
142
136
  ```ruby
143
- ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
144
- ecdsa_key.generate_key
145
- ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
146
- ecdsa_public.private_key = nil
137
+ ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
147
138
 
148
139
  token = JWT.encode payload, ecdsa_key, 'ES256'
149
140
 
150
141
  # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
151
142
  puts token
152
143
 
153
- decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
144
+ decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
154
145
 
155
146
  # Array
156
147
  # [
@@ -160,7 +151,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
160
151
  puts decoded_token
161
152
  ```
162
153
 
163
- **EDDSA**
154
+ ### **EDDSA**
164
155
 
165
156
  In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
166
157
 
@@ -168,7 +159,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
168
159
  gem 'rbnacl'
169
160
  ```
170
161
 
171
- 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.
172
163
 
173
164
  * ED25519
174
165
 
@@ -189,9 +180,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
189
180
 
190
181
  ```
191
182
 
192
- **RSASSA-PSS**
183
+ ### **RSASSA-PSS**
193
184
 
194
- 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`.
195
186
 
196
187
  ```ruby
197
188
  gem 'openssl', '~> 2.1'
@@ -220,6 +211,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
220
211
  puts decoded_token
221
212
  ```
222
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
+
223
241
  ## Support for reserved claim names
224
242
  JSON Web Token defines some reserved claim names and defines how they should be
225
243
  used. JWT supports these reserved claim names:
@@ -285,6 +303,12 @@ rescue JWT::ExpiredSignature
285
303
  end
286
304
  ```
287
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
+
288
312
  **Adding Leeway**
289
313
 
290
314
  ```ruby
@@ -325,6 +349,12 @@ rescue JWT::ImmatureSignature
325
349
  end
326
350
  ```
327
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
+
328
358
  **Adding Leeway**
329
359
 
330
360
  ```ruby
@@ -366,6 +396,36 @@ rescue JWT::InvalidIssuerError
366
396
  end
367
397
  ```
368
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
+
369
429
  ### Audience Claim
370
430
 
371
431
  From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
@@ -406,6 +466,8 @@ begin
406
466
  #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
407
467
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
408
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' }
409
471
  rescue JWT::InvalidJtiError
410
472
  # Handle invalid token, e.g. logout user or deny access
411
473
  puts 'Error'
@@ -454,24 +516,115 @@ rescue JWT::InvalidSubError
454
516
  end
455
517
  ```
456
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
+
457
570
  ### JSON Web Key (JWK)
458
571
 
459
- 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):
460
589
 
461
590
  ```ruby
462
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
463
- 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
+ ```
464
596
 
465
- token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
466
597
 
467
- # The jwk loader would fetch the set of JWKs from a trusted source
468
- jwk_loader = ->(options) do
469
- @cached_keys = nil if options[:invalidate] # need to reload the keys
470
- @cached_keys ||= { keys: [jwk.export] }
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.
600
+
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.
603
+
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
471
624
  end
472
625
 
473
626
  begin
474
- JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
627
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
475
628
  rescue JWT::JWKError
476
629
  # Handle problems with the provided JWKs
477
630
  rescue JWT::DecodeError
@@ -479,6 +632,54 @@ rescue JWT::DecodeError
479
632
  end
480
633
  ```
481
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
+
482
683
  # Development and Tests
483
684
 
484
685
  We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
@@ -487,18 +688,20 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
487
688
  rake release
488
689
  ```
489
690
 
490
- 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.
491
692
 
492
693
  ```bash
493
- bundle exec rspec
694
+ bundle install
695
+ bundle exec appraisal rake test
494
696
  ```
495
697
 
496
- **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).
497
700
 
498
701
  ## Contributors
499
702
 
500
- See `AUTHORS` file.
703
+ See [AUTHORS](AUTHORS).
501
704
 
502
705
  ## License
503
706
 
504
- 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
@@ -1,34 +1,75 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  module Algos
3
5
  module Ecdsa
4
6
  module_function
5
7
 
6
- SUPPORTED = %w[ES256 ES384 ES512].freeze
7
8
  NAMED_CURVES = {
8
- 'prime256v1' => 'ES256',
9
- 'secp384r1' => 'ES384',
10
- 'secp521r1' => 'ES512'
9
+ 'prime256v1' => {
10
+ algorithm: 'ES256',
11
+ digest: 'sha256'
12
+ },
13
+ 'secp256r1' => { # alias for prime256v1
14
+ algorithm: 'ES256',
15
+ digest: 'sha256'
16
+ },
17
+ 'secp384r1' => {
18
+ algorithm: 'ES384',
19
+ digest: 'sha384'
20
+ },
21
+ 'secp521r1' => {
22
+ algorithm: 'ES512',
23
+ digest: 'sha512'
24
+ },
25
+ 'secp256k1' => {
26
+ algorithm: 'ES256K',
27
+ digest: 'sha256'
28
+ }
11
29
  }.freeze
12
30
 
13
- def sign(to_sign)
14
- algorithm, msg, key = to_sign.values
15
- key_algorithm = NAMED_CURVES[key.group.curve_name]
31
+ SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
32
+
33
+ def sign(algorithm, msg, key)
34
+ curve_definition = curve_by_name(key.group.curve_name)
35
+ key_algorithm = curve_definition[:algorithm]
16
36
  if algorithm != key_algorithm
17
37
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
18
38
  end
19
39
 
20
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
21
- SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
40
+ digest = OpenSSL::Digest.new(curve_definition[:digest])
41
+ asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
22
42
  end
23
43
 
24
- def verify(to_verify)
25
- algorithm, public_key, signing_input, signature = to_verify.values
26
- key_algorithm = NAMED_CURVES[public_key.group.curve_name]
44
+ def verify(algorithm, public_key, signing_input, signature)
45
+ curve_definition = curve_by_name(public_key.group.curve_name)
46
+ key_algorithm = curve_definition[:algorithm]
27
47
  if algorithm != key_algorithm
28
48
  raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
29
49
  end
30
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
31
- public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
50
+
51
+ digest = OpenSSL::Digest.new(curve_definition[:digest])
52
+ public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
53
+ rescue OpenSSL::PKey::PKeyError
54
+ raise JWT::VerificationError, 'Signature verification raised'
55
+ end
56
+
57
+ def curve_by_name(name)
58
+ NAMED_CURVES.fetch(name) do
59
+ raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
60
+ end
61
+ end
62
+
63
+ def raw_to_asn1(signature, private_key)
64
+ byte_size = (private_key.group.degree + 7) / 8
65
+ sig_bytes = signature[0..(byte_size - 1)]
66
+ sig_char = signature[byte_size..-1] || ''
67
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
68
+ end
69
+
70
+ def asn1_to_raw(signature, public_key)
71
+ byte_size = (public_key.group.degree + 7) / 8
72
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
32
73
  end
33
74
  end
34
75
  end
@@ -1,22 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  module Algos
3
5
  module Eddsa
4
6
  module_function
5
7
 
6
- SUPPORTED = %w[ED25519].freeze
8
+ SUPPORTED = %w[ED25519 EdDSA].freeze
9
+
10
+ def sign(algorithm, msg, key)
11
+ if key.class != RbNaCl::Signatures::Ed25519::SigningKey
12
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
13
+ end
14
+ unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
15
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
16
+ end
7
17
 
8
- def sign(to_sign)
9
- algorithm, msg, key = to_sign.values
10
- raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive
12
18
  key.sign(msg)
13
19
  end
14
20
 
15
- def verify(to_verify)
16
- algorithm, public_key, signing_input, signature = to_verify.values
17
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive
21
+ def verify(algorithm, public_key, signing_input, signature)
22
+ unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
23
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
24
+ end
18
25
  raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
26
+
19
27
  public_key.verify(signature, signing_input)
28
+ rescue RbNaCl::CryptoError
29
+ false
20
30
  end
21
31
  end
22
32
  end