jwt 2.3.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AUTHORS +60 -53
- data/CHANGELOG.md +73 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +188 -40
- data/lib/jwt/algos/algo_wrapper.rb +30 -0
- data/lib/jwt/algos/ecdsa.rb +39 -12
- data/lib/jwt/algos/eddsa.rb +7 -4
- data/lib/jwt/algos/hmac.rb +56 -17
- data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
- data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
- data/lib/jwt/algos/none.rb +5 -1
- data/lib/jwt/algos/ps.rb +6 -8
- data/lib/jwt/algos/rsa.rb +7 -5
- data/lib/jwt/algos/unsupported.rb +2 -0
- data/lib/jwt/algos.rb +38 -15
- data/lib/jwt/claims_validator.rb +3 -1
- data/lib/jwt/configuration/container.rb +21 -0
- data/lib/jwt/configuration/decode_configuration.rb +46 -0
- data/lib/jwt/configuration/jwk_configuration.rb +27 -0
- data/lib/jwt/configuration.rb +15 -0
- data/lib/jwt/decode.rb +83 -26
- data/lib/jwt/encode.rb +30 -20
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/jwk/ec.rb +147 -61
- data/lib/jwt/jwk/hmac.rb +69 -24
- data/lib/jwt/jwk/key_base.rb +43 -6
- data/lib/jwt/jwk/key_finder.rb +19 -35
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
- data/lib/jwt/jwk/rsa.rb +142 -54
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +15 -11
- data/lib/jwt/security_utils.rb +2 -27
- data/lib/jwt/verify.rb +10 -2
- data/lib/jwt/version.rb +22 -2
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +5 -4
- data/ruby-jwt.gemspec +12 -5
- metadata +20 -16
- data/.github/workflows/test.yml +0 -74
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -97
- data/.rubocop_todo.yml +0 -185
- data/.sourcelevel.yml +0 -18
- data/Appraisals +0 -10
- data/Gemfile +0 -5
- data/Rakefile +0 -14
- data/lib/jwt/default_options.rb +0 -16
- data/lib/jwt/signature.rb +0 -39
data/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# JWT
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/jwt)
|
|
4
|
-
[](https://github.com/jwt/ruby-jwt/actions)
|
|
5
5
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
|
6
6
|
[](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
|
|
7
7
|
[](https://codeclimate.com/github/jwt/ruby-jwt)
|
|
8
|
-
[](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
|
|
9
8
|
|
|
10
9
|
A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
|
|
11
10
|
|
|
12
11
|
If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt).
|
|
13
12
|
|
|
14
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
|
+
|
|
19
20
|
## Sponsors
|
|
20
21
|
|
|
21
22
|
|Logo|Message|
|
|
@@ -42,7 +43,7 @@ The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cr
|
|
|
42
43
|
|
|
43
44
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
|
44
45
|
|
|
45
|
-
**NONE**
|
|
46
|
+
### **NONE**
|
|
46
47
|
|
|
47
48
|
* none - unsigned token
|
|
48
49
|
|
|
@@ -68,7 +69,7 @@ decoded_token = JWT.decode token, nil, false
|
|
|
68
69
|
puts decoded_token
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
**HMAC**
|
|
72
|
+
### **HMAC**
|
|
72
73
|
|
|
73
74
|
* HS256 - HMAC using SHA-256 hash algorithm
|
|
74
75
|
* HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
|
|
@@ -76,7 +77,7 @@ puts decoded_token
|
|
|
76
77
|
* HS512 - HMAC using SHA-512 hash algorithm
|
|
77
78
|
|
|
78
79
|
```ruby
|
|
79
|
-
# The secret must be a string.
|
|
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.
|
|
80
81
|
hmac_secret = 'my$ecretK3y'
|
|
81
82
|
|
|
82
83
|
token = JWT.encode payload, hmac_secret, 'HS256'
|
|
@@ -94,13 +95,13 @@ decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
|
|
94
95
|
puts decoded_token
|
|
95
96
|
```
|
|
96
97
|
|
|
97
|
-
Note: If [RbNaCl](https://github.com/
|
|
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.
|
|
98
99
|
|
|
99
|
-
[RbNaCl](https://github.com/
|
|
100
|
+
[RbNaCl](https://github.com/RubyCrypto/rbnacl) requires
|
|
100
101
|
[libsodium](https://github.com/jedisct1/libsodium), it can be installed
|
|
101
102
|
on MacOS with `brew install libsodium`.
|
|
102
103
|
|
|
103
|
-
**RSA**
|
|
104
|
+
### **RSA**
|
|
104
105
|
|
|
105
106
|
* RS256 - RSA using SHA-256 hash algorithm
|
|
106
107
|
* RS384 - RSA using SHA-384 hash algorithm
|
|
@@ -125,24 +126,22 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
|
|
|
125
126
|
puts decoded_token
|
|
126
127
|
```
|
|
127
128
|
|
|
128
|
-
**ECDSA**
|
|
129
|
+
### **ECDSA**
|
|
129
130
|
|
|
130
131
|
* ES256 - ECDSA using P-256 and SHA-256
|
|
131
132
|
* ES384 - ECDSA using P-384 and SHA-384
|
|
132
133
|
* ES512 - ECDSA using P-521 and SHA-512
|
|
134
|
+
* ES256K - ECDSA using P-256K and SHA-256
|
|
133
135
|
|
|
134
136
|
```ruby
|
|
135
|
-
ecdsa_key = OpenSSL::PKey::EC.
|
|
136
|
-
ecdsa_key.generate_key
|
|
137
|
-
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
|
|
138
|
-
ecdsa_public.private_key = nil
|
|
137
|
+
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
139
138
|
|
|
140
139
|
token = JWT.encode payload, ecdsa_key, 'ES256'
|
|
141
140
|
|
|
142
141
|
# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
|
|
143
142
|
puts token
|
|
144
143
|
|
|
145
|
-
decoded_token = JWT.decode token,
|
|
144
|
+
decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
|
|
146
145
|
|
|
147
146
|
# Array
|
|
148
147
|
# [
|
|
@@ -152,7 +151,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
|
|
|
152
151
|
puts decoded_token
|
|
153
152
|
```
|
|
154
153
|
|
|
155
|
-
**EDDSA**
|
|
154
|
+
### **EDDSA**
|
|
156
155
|
|
|
157
156
|
In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
|
|
158
157
|
|
|
@@ -160,7 +159,7 @@ In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`
|
|
|
160
159
|
gem 'rbnacl'
|
|
161
160
|
```
|
|
162
161
|
|
|
163
|
-
For more detailed installation instruction check the official [repository](https://github.com/
|
|
162
|
+
For more detailed installation instruction check the official [repository](https://github.com/RubyCrypto/rbnacl) on GitHub.
|
|
164
163
|
|
|
165
164
|
* ED25519
|
|
166
165
|
|
|
@@ -181,9 +180,9 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
|
|
|
181
180
|
|
|
182
181
|
```
|
|
183
182
|
|
|
184
|
-
**RSASSA-PSS**
|
|
183
|
+
### **RSASSA-PSS**
|
|
185
184
|
|
|
186
|
-
In order to use this algorithm you need to add the `openssl` gem to
|
|
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`.
|
|
187
186
|
|
|
188
187
|
```ruby
|
|
189
188
|
gem 'openssl', '~> 2.1'
|
|
@@ -212,6 +211,33 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }
|
|
|
212
211
|
puts decoded_token
|
|
213
212
|
```
|
|
214
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
|
+
|
|
215
241
|
## Support for reserved claim names
|
|
216
242
|
JSON Web Token defines some reserved claim names and defines how they should be
|
|
217
243
|
used. JWT supports these reserved claim names:
|
|
@@ -370,6 +396,36 @@ rescue JWT::InvalidIssuerError
|
|
|
370
396
|
end
|
|
371
397
|
```
|
|
372
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
|
+
|
|
373
429
|
### Audience Claim
|
|
374
430
|
|
|
375
431
|
From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
|
|
@@ -486,28 +542,89 @@ end
|
|
|
486
542
|
|
|
487
543
|
You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
|
|
488
544
|
```ruby
|
|
489
|
-
# Will raise a JWT::
|
|
545
|
+
# Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
|
|
490
546
|
JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
|
|
491
547
|
```
|
|
492
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
|
+
|
|
493
570
|
### JSON Web Key (JWK)
|
|
494
571
|
|
|
495
|
-
JWK is a JSON structure representing a cryptographic key.
|
|
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):
|
|
496
589
|
|
|
497
590
|
```ruby
|
|
498
|
-
|
|
499
|
-
|
|
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
|
+
```
|
|
500
596
|
|
|
501
|
-
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
|
|
502
597
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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_jwks` 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
|
|
507
624
|
end
|
|
508
625
|
|
|
509
626
|
begin
|
|
510
|
-
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks:
|
|
627
|
+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
|
|
511
628
|
rescue JWT::JWKError
|
|
512
629
|
# Handle problems with the provided JWKs
|
|
513
630
|
rescue JWT::DecodeError
|
|
@@ -515,22 +632,52 @@ rescue JWT::DecodeError
|
|
|
515
632
|
end
|
|
516
633
|
```
|
|
517
634
|
|
|
518
|
-
|
|
635
|
+
### Importing and exporting JSON Web Keys
|
|
519
636
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
523
663
|
```
|
|
524
664
|
|
|
525
|
-
###
|
|
665
|
+
### Key ID (kid) and JWKs
|
|
526
666
|
|
|
527
|
-
The
|
|
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.
|
|
528
670
|
|
|
529
671
|
```ruby
|
|
530
|
-
jwk =
|
|
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)
|
|
531
677
|
|
|
532
678
|
jwk_hash = jwk.export
|
|
533
|
-
|
|
679
|
+
|
|
680
|
+
thumbprint_as_the_kid = jwk_hash[:kid]
|
|
534
681
|
```
|
|
535
682
|
|
|
536
683
|
# Development and Tests
|
|
@@ -548,12 +695,13 @@ bundle install
|
|
|
548
695
|
bundle exec appraisal rake test
|
|
549
696
|
```
|
|
550
697
|
|
|
551
|
-
|
|
698
|
+
## How to contribute
|
|
699
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
|
552
700
|
|
|
553
701
|
## Contributors
|
|
554
702
|
|
|
555
|
-
See
|
|
703
|
+
See [AUTHORS](AUTHORS).
|
|
556
704
|
|
|
557
705
|
## License
|
|
558
706
|
|
|
559
|
-
See
|
|
707
|
+
See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
rescue OpenSSL::PKey::PKeyError # These should be moved to the algorithms that actually need this, but left here to ensure nothing will break.
|
|
24
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
|
25
|
+
ensure
|
|
26
|
+
OpenSSL.errors.clear
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/jwt/algos/ecdsa.rb
CHANGED
|
@@ -1,35 +1,62 @@
|
|
|
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' =>
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
40
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
|
21
41
|
SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
|
22
42
|
end
|
|
23
43
|
|
|
24
|
-
def verify(
|
|
25
|
-
|
|
26
|
-
key_algorithm =
|
|
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
|
-
|
|
50
|
+
|
|
51
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
|
31
52
|
public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
|
|
32
53
|
end
|
|
54
|
+
|
|
55
|
+
def curve_by_name(name)
|
|
56
|
+
NAMED_CURVES.fetch(name) do
|
|
57
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
33
60
|
end
|
|
34
61
|
end
|
|
35
62
|
end
|
data/lib/jwt/algos/eddsa.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JWT
|
|
2
4
|
module Algos
|
|
3
5
|
module Eddsa
|
|
@@ -5,8 +7,7 @@ module JWT
|
|
|
5
7
|
|
|
6
8
|
SUPPORTED = %w[ED25519 EdDSA].freeze
|
|
7
9
|
|
|
8
|
-
def sign(
|
|
9
|
-
algorithm, msg, key = to_sign.values
|
|
10
|
+
def sign(algorithm, msg, key)
|
|
10
11
|
if key.class != RbNaCl::Signatures::Ed25519::SigningKey
|
|
11
12
|
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
|
|
12
13
|
end
|
|
@@ -17,13 +18,15 @@ module JWT
|
|
|
17
18
|
key.sign(msg)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
def verify(
|
|
21
|
-
algorithm, public_key, signing_input, signature = to_verify.values
|
|
21
|
+
def verify(algorithm, public_key, signing_input, signature)
|
|
22
22
|
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
|
23
23
|
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
|
24
24
|
end
|
|
25
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
|
+
|
|
26
27
|
public_key.verify(signature, signing_input)
|
|
28
|
+
rescue RbNaCl::CryptoError
|
|
29
|
+
false
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
data/lib/jwt/algos/hmac.rb
CHANGED
|
@@ -1,34 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module JWT
|
|
2
4
|
module Algos
|
|
3
5
|
module Hmac
|
|
4
6
|
module_function
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
MAPPING = {
|
|
9
|
+
'HS256' => OpenSSL::Digest::SHA256,
|
|
10
|
+
'HS384' => OpenSSL::Digest::SHA384,
|
|
11
|
+
'HS512' => OpenSSL::Digest::SHA512
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
SUPPORTED = MAPPING.keys
|
|
7
15
|
|
|
8
|
-
def sign(
|
|
9
|
-
algorithm, msg, key = to_sign.values
|
|
16
|
+
def sign(algorithm, msg, key)
|
|
10
17
|
key ||= ''
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
|
|
20
|
+
|
|
21
|
+
OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg)
|
|
22
|
+
rescue OpenSSL::HMACError => e
|
|
23
|
+
if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
|
|
24
|
+
raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
|
|
16
25
|
end
|
|
26
|
+
|
|
27
|
+
raise e
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def verify(algorithm, key, signing_input, signature)
|
|
31
|
+
SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key))
|
|
17
32
|
end
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
|
|
35
|
+
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
|
36
|
+
module SecurityUtils
|
|
37
|
+
# Constant time string comparison, for fixed length strings.
|
|
38
|
+
#
|
|
39
|
+
# The values compared should be of fixed length, such as strings
|
|
40
|
+
# that have already been processed by HMAC. Raises in case of length mismatch.
|
|
41
|
+
|
|
42
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
|
43
|
+
def fixed_length_secure_compare(a, b)
|
|
44
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
27
45
|
end
|
|
28
46
|
else
|
|
29
|
-
|
|
47
|
+
def fixed_length_secure_compare(a, b)
|
|
48
|
+
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
|
|
49
|
+
|
|
50
|
+
l = a.unpack "C#{a.bytesize}"
|
|
51
|
+
|
|
52
|
+
res = 0
|
|
53
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
|
54
|
+
res == 0
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
module_function :fixed_length_secure_compare
|
|
58
|
+
|
|
59
|
+
# Secure string comparison for strings of variable length.
|
|
60
|
+
#
|
|
61
|
+
# While a timing attack would not be able to discern the content of
|
|
62
|
+
# a secret compared via secure_compare, it is possible to determine
|
|
63
|
+
# the secret length. This should be considered when using secure_compare
|
|
64
|
+
# to compare weak, short secrets to user input.
|
|
65
|
+
def secure_compare(a, b)
|
|
66
|
+
a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
|
|
30
67
|
end
|
|
68
|
+
module_function :secure_compare
|
|
31
69
|
end
|
|
70
|
+
# rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
|
32
71
|
end
|
|
33
72
|
end
|
|
34
73
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWT
|
|
4
|
+
module Algos
|
|
5
|
+
module HmacRbNaCl
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
MAPPING = {
|
|
9
|
+
'HS256' => ::RbNaCl::HMAC::SHA256,
|
|
10
|
+
'HS512256' => ::RbNaCl::HMAC::SHA512256,
|
|
11
|
+
'HS384' => nil,
|
|
12
|
+
'HS512' => ::RbNaCl::HMAC::SHA512
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
SUPPORTED = MAPPING.keys
|
|
16
|
+
|
|
17
|
+
def sign(algorithm, msg, key)
|
|
18
|
+
if (hmac = resolve_algorithm(algorithm))
|
|
19
|
+
hmac.auth(key_for_rbnacl(hmac, key).encode('binary'), msg.encode('binary'))
|
|
20
|
+
else
|
|
21
|
+
Hmac.sign(algorithm, msg, key)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify(algorithm, key, signing_input, signature)
|
|
26
|
+
if (hmac = resolve_algorithm(algorithm))
|
|
27
|
+
hmac.verify(key_for_rbnacl(hmac, key).encode('binary'), signature.encode('binary'), signing_input.encode('binary'))
|
|
28
|
+
else
|
|
29
|
+
Hmac.verify(algorithm, key, signing_input, signature)
|
|
30
|
+
end
|
|
31
|
+
rescue ::RbNaCl::BadAuthenticatorError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_for_rbnacl(hmac, key)
|
|
36
|
+
key ||= ''
|
|
37
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
|
|
38
|
+
|
|
39
|
+
return padded_empty_key(hmac.key_bytes) if key == ''
|
|
40
|
+
|
|
41
|
+
key
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_algorithm(algorithm)
|
|
45
|
+
MAPPING.fetch(algorithm)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def padded_empty_key(length)
|
|
49
|
+
Array.new(length, 0x0).pack('C*').encode('binary')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|