jwt 2.2.3 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +8 -0
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +3 -11
- data/.gitignore +2 -0
- data/.rubocop.yml +12 -28
- data/.rubocop_todo.yml +9 -178
- data/AUTHORS +31 -13
- data/Appraisals +3 -0
- data/CHANGELOG.md +85 -2
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +3 -1
- data/README.md +92 -25
- data/Rakefile +2 -0
- data/lib/jwt/algos/ecdsa.rb +23 -5
- data/lib/jwt/algos/eddsa.rb +14 -4
- data/lib/jwt/algos/hmac.rb +2 -0
- data/lib/jwt/algos/none.rb +2 -0
- data/lib/jwt/algos/ps.rb +3 -3
- data/lib/jwt/algos/rsa.rb +4 -1
- data/lib/jwt/algos/unsupported.rb +2 -0
- data/lib/jwt/claims_validator.rb +3 -1
- data/lib/jwt/decode.rb +44 -8
- data/lib/jwt/default_options.rb +4 -1
- data/lib/jwt/encode.rb +6 -6
- data/lib/jwt/error.rb +2 -0
- data/lib/jwt/jwk/ec.rb +7 -7
- data/lib/jwt/jwk/hmac.rb +1 -1
- data/lib/jwt/jwk/key_base.rb +1 -0
- data/lib/jwt/jwk/rsa.rb +4 -3
- data/lib/jwt/jwk.rb +3 -2
- data/lib/jwt/security_utils.rb +2 -0
- data/lib/jwt/signature.rb +3 -7
- data/lib/jwt/verify.rb +18 -3
- data/lib/jwt/version.rb +2 -3
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +1 -1
- data/ruby-jwt.gemspec +8 -2
- metadata +11 -6
- data/lib/jwt/base64.rb +0 -19
data/CODE_OF_CONDUCT.md
ADDED
@@ -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/Gemfile
CHANGED
data/README.md
CHANGED
@@ -38,11 +38,11 @@ And run `bundle install`
|
|
38
38
|
|
39
39
|
## Algorithms and Usage
|
40
40
|
|
41
|
-
The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/
|
41
|
+
The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
|
42
42
|
|
43
43
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
44
44
|
|
45
|
-
**NONE**
|
45
|
+
### **NONE**
|
46
46
|
|
47
47
|
* none - unsigned token
|
48
48
|
|
@@ -68,7 +68,7 @@ decoded_token = JWT.decode token, nil, false
|
|
68
68
|
puts decoded_token
|
69
69
|
```
|
70
70
|
|
71
|
-
**HMAC**
|
71
|
+
### **HMAC**
|
72
72
|
|
73
73
|
* HS256 - HMAC using SHA-256 hash algorithm
|
74
74
|
* HS512256 - HMAC using SHA-512-256 hash algorithm (only available with RbNaCl; see note below)
|
@@ -76,6 +76,7 @@ puts decoded_token
|
|
76
76
|
* HS512 - HMAC using SHA-512 hash algorithm
|
77
77
|
|
78
78
|
```ruby
|
79
|
+
# The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
|
79
80
|
hmac_secret = 'my$ecretK3y'
|
80
81
|
|
81
82
|
token = JWT.encode payload, hmac_secret, 'HS256'
|
@@ -85,21 +86,6 @@ puts token
|
|
85
86
|
|
86
87
|
decoded_token = JWT.decode token, hmac_secret, true, { algorithm: 'HS256' }
|
87
88
|
|
88
|
-
# Array
|
89
|
-
# [
|
90
|
-
# {"data"=>"test"}, # payload
|
91
|
-
# {"alg"=>"HS256"} # header
|
92
|
-
# ]
|
93
|
-
puts decoded_token
|
94
|
-
|
95
|
-
# Without secret key
|
96
|
-
token = JWT.encode payload, nil, 'HS256'
|
97
|
-
|
98
|
-
# eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pVzcY2dX8JNM3LzIYeP2B1e1Wcpt1K3TWVvIYSF4x-o
|
99
|
-
puts token
|
100
|
-
|
101
|
-
decoded_token = JWT.decode token, nil, true, { algorithm: 'HS256' }
|
102
|
-
|
103
89
|
# Array
|
104
90
|
# [
|
105
91
|
# {"data"=>"test"}, # payload
|
@@ -114,7 +100,7 @@ Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt
|
|
114
100
|
[libsodium](https://github.com/jedisct1/libsodium), it can be installed
|
115
101
|
on MacOS with `brew install libsodium`.
|
116
102
|
|
117
|
-
**RSA**
|
103
|
+
### **RSA**
|
118
104
|
|
119
105
|
* RS256 - RSA using SHA-256 hash algorithm
|
120
106
|
* RS384 - RSA using SHA-384 hash algorithm
|
@@ -139,7 +125,7 @@ decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
|
|
139
125
|
puts decoded_token
|
140
126
|
```
|
141
127
|
|
142
|
-
**ECDSA**
|
128
|
+
### **ECDSA**
|
143
129
|
|
144
130
|
* ES256 - ECDSA using P-256 and SHA-256
|
145
131
|
* ES384 - ECDSA using P-384 and SHA-384
|
@@ -166,7 +152,7 @@ decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
|
|
166
152
|
puts decoded_token
|
167
153
|
```
|
168
154
|
|
169
|
-
**EDDSA**
|
155
|
+
### **EDDSA**
|
170
156
|
|
171
157
|
In order to use this algorithm you need to add the `RbNaCl` gem to you `Gemfile`.
|
172
158
|
|
@@ -195,7 +181,7 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }
|
|
195
181
|
|
196
182
|
```
|
197
183
|
|
198
|
-
**RSASSA-PSS**
|
184
|
+
### **RSASSA-PSS**
|
199
185
|
|
200
186
|
In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`.
|
201
187
|
|
@@ -384,6 +370,36 @@ rescue JWT::InvalidIssuerError
|
|
384
370
|
end
|
385
371
|
```
|
386
372
|
|
373
|
+
You can also pass a Regexp or Proc (with arity 1), verification will pass if the regexp matches or the proc returns truthy.
|
374
|
+
On supported ruby versions (>= 2.5) you can also delegate to methods, on older versions you will have
|
375
|
+
to convert them to proc (using `to_proc`)
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
JWT.decode token, hmac_secret, true,
|
379
|
+
iss: %r'https://my.awesome.website/',
|
380
|
+
verify_iss: true,
|
381
|
+
algorithm: 'HS256'
|
382
|
+
```
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
JWT.decode token, hmac_secret, true,
|
386
|
+
iss: ->(issuer) { issuer.start_with?('My Awesome Company Inc') },
|
387
|
+
verify_iss: true,
|
388
|
+
algorithm: 'HS256'
|
389
|
+
```
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
JWT.decode token, hmac_secret, true,
|
393
|
+
iss: method(:valid_issuer?),
|
394
|
+
verify_iss: true,
|
395
|
+
algorithm: 'HS256'
|
396
|
+
|
397
|
+
# somewhere in the same class:
|
398
|
+
def valid_issuer?(issuer)
|
399
|
+
# custom validation
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
387
403
|
### Audience Claim
|
388
404
|
|
389
405
|
From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3):
|
@@ -474,12 +490,63 @@ rescue JWT::InvalidSubError
|
|
474
490
|
end
|
475
491
|
```
|
476
492
|
|
493
|
+
### Finding a Key
|
494
|
+
|
495
|
+
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.
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
|
499
|
+
iss_payload = { data: 'data', iss: issuers.first }
|
500
|
+
|
501
|
+
secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
|
502
|
+
|
503
|
+
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
504
|
+
|
505
|
+
begin
|
506
|
+
# Add iss to the validation to check if the token has been manipulated
|
507
|
+
decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
|
508
|
+
secrets[payload['iss']]
|
509
|
+
end
|
510
|
+
rescue JWT::InvalidIssuerError
|
511
|
+
# Handle invalid token, e.g. logout user or deny access
|
512
|
+
end
|
513
|
+
```
|
514
|
+
|
515
|
+
### Required Claims
|
516
|
+
|
517
|
+
You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
|
518
|
+
```ruby
|
519
|
+
# Will raise a JWT::MissingRequiredClaim error if the 'exp' claim is absent
|
520
|
+
JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
|
521
|
+
```
|
522
|
+
|
523
|
+
### X.509 certificates in x5c header
|
524
|
+
|
525
|
+
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).
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
root_certificates = [] # trusted `OpenSSL::X509::Certificate` objects
|
529
|
+
crl_uris = root_certificates.map(&:crl_uris)
|
530
|
+
crls = crl_uris.map do |uri|
|
531
|
+
# look up cached CRL by `uri` and return it if found, otherwise continue
|
532
|
+
crl = Net::HTTP.get(uri)
|
533
|
+
crl = OpenSSL::X509::CRL.new(crl)
|
534
|
+
# cache `crl` using `uri` as the key, expiry set to `crl.next_update` timestamp
|
535
|
+
end
|
536
|
+
|
537
|
+
begin
|
538
|
+
JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls })
|
539
|
+
rescue JWT::DecodeError
|
540
|
+
# Handle error, e.g. x5c header certificate revoked or expired
|
541
|
+
end
|
542
|
+
```
|
543
|
+
|
477
544
|
### JSON Web Key (JWK)
|
478
545
|
|
479
|
-
JWK is a JSON structure representing a cryptographic key. Currently only supports RSA
|
546
|
+
JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys.
|
480
547
|
|
481
548
|
```ruby
|
482
|
-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
549
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
|
483
550
|
payload, headers = { data: 'data' }, { kid: jwk.kid }
|
484
551
|
|
485
552
|
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
|
@@ -502,7 +569,7 @@ end
|
|
502
569
|
or by passing JWK as a simple Hash
|
503
570
|
|
504
571
|
```
|
505
|
-
jwks = { keys: [{ ... }] } # keys
|
572
|
+
jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
|
506
573
|
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
|
507
574
|
```
|
508
575
|
|
data/Rakefile
CHANGED
data/lib/jwt/algos/ecdsa.rb
CHANGED
@@ -1,35 +1,53 @@
|
|
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
9
|
'prime256v1' => 'ES256',
|
10
|
+
'secp256r1' => 'ES256', # alias for prime256v1
|
9
11
|
'secp384r1' => 'ES384',
|
10
12
|
'secp521r1' => 'ES512'
|
11
13
|
}.freeze
|
12
14
|
|
15
|
+
SUPPORTED = NAMED_CURVES.values.uniq.freeze
|
16
|
+
|
13
17
|
def sign(to_sign)
|
14
18
|
algorithm, msg, key = to_sign.values
|
15
|
-
|
19
|
+
curve_definition = curve_by_name(key.group.curve_name)
|
20
|
+
key_algorithm = curve_definition[:algorithm]
|
16
21
|
if algorithm != key_algorithm
|
17
22
|
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
|
18
23
|
end
|
19
24
|
|
20
|
-
digest = OpenSSL::Digest.new(
|
25
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
21
26
|
SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
22
27
|
end
|
23
28
|
|
24
29
|
def verify(to_verify)
|
25
30
|
algorithm, public_key, signing_input, signature = to_verify.values
|
26
|
-
|
31
|
+
curve_definition = curve_by_name(public_key.group.curve_name)
|
32
|
+
key_algorithm = curve_definition[:algorithm]
|
27
33
|
if algorithm != key_algorithm
|
28
34
|
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
|
29
35
|
end
|
30
|
-
|
36
|
+
|
37
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
31
38
|
public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
|
32
39
|
end
|
40
|
+
|
41
|
+
def curve_by_name(name)
|
42
|
+
algorithm = NAMED_CURVES.fetch(name) do
|
43
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
44
|
+
end
|
45
|
+
|
46
|
+
{
|
47
|
+
algorithm: algorithm,
|
48
|
+
digest: algorithm.sub('ES', 'sha')
|
49
|
+
}
|
50
|
+
end
|
33
51
|
end
|
34
52
|
end
|
35
53
|
end
|
data/lib/jwt/algos/eddsa.rb
CHANGED
@@ -1,21 +1,31 @@
|
|
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
|
7
9
|
|
8
10
|
def sign(to_sign)
|
9
11
|
algorithm, msg, key = to_sign.values
|
10
|
-
|
11
|
-
|
12
|
+
if key.class != RbNaCl::Signatures::Ed25519::SigningKey
|
13
|
+
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
|
14
|
+
end
|
15
|
+
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
16
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
17
|
+
end
|
18
|
+
|
12
19
|
key.sign(msg)
|
13
20
|
end
|
14
21
|
|
15
22
|
def verify(to_verify)
|
16
23
|
algorithm, public_key, signing_input, signature = to_verify.values
|
17
|
-
|
24
|
+
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
25
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
26
|
+
end
|
18
27
|
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
|
28
|
+
|
19
29
|
public_key.verify(signature, signing_input)
|
20
30
|
end
|
21
31
|
end
|
data/lib/jwt/algos/hmac.rb
CHANGED
data/lib/jwt/algos/none.rb
CHANGED
data/lib/jwt/algos/ps.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Ps
|
@@ -29,9 +31,7 @@ module JWT
|
|
29
31
|
|
30
32
|
def require_openssl!
|
31
33
|
if Object.const_defined?('OpenSSL')
|
32
|
-
|
33
|
-
|
34
|
-
unless major.to_i >= 2 && minor.to_i >= 1
|
34
|
+
if ::Gem::Version.new(OpenSSL::VERSION) < ::Gem::Version.new('2.1')
|
35
35
|
raise JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
|
36
36
|
end
|
37
37
|
else
|
data/lib/jwt/algos/rsa.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Rsa
|
@@ -7,7 +9,8 @@ module JWT
|
|
7
9
|
|
8
10
|
def sign(to_sign)
|
9
11
|
algorithm, msg, key = to_sign.values
|
10
|
-
raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.
|
12
|
+
raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.instance_of?(String)
|
13
|
+
|
11
14
|
key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
|
12
15
|
end
|
13
16
|
|
data/lib/jwt/claims_validator.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './error'
|
2
4
|
|
3
5
|
module JWT
|
@@ -9,7 +11,7 @@ module JWT
|
|
9
11
|
].freeze
|
10
12
|
|
11
13
|
def initialize(payload)
|
12
|
-
@payload = payload.
|
14
|
+
@payload = payload.transform_keys(&:to_sym)
|
13
15
|
end
|
14
16
|
|
15
17
|
def validate!
|
data/lib/jwt/decode.rb
CHANGED
@@ -4,12 +4,14 @@ require 'json'
|
|
4
4
|
|
5
5
|
require 'jwt/signature'
|
6
6
|
require 'jwt/verify'
|
7
|
+
require 'jwt/x5c_key_finder'
|
7
8
|
# JWT::Decode module
|
8
9
|
module JWT
|
9
10
|
# Decoding logic for JWT
|
10
11
|
class Decode
|
11
12
|
def initialize(jwt, key, verify, options, &keyfinder)
|
12
13
|
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
14
|
+
|
13
15
|
@jwt = jwt
|
14
16
|
@key = key
|
15
17
|
@options = options
|
@@ -23,27 +25,50 @@ module JWT
|
|
23
25
|
validate_segment_count!
|
24
26
|
if @verify
|
25
27
|
decode_crypto
|
28
|
+
verify_algo
|
29
|
+
set_key
|
26
30
|
verify_signature
|
27
31
|
verify_claims
|
28
32
|
end
|
29
33
|
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
34
|
+
|
30
35
|
[payload, header]
|
31
36
|
end
|
32
37
|
|
33
38
|
private
|
34
39
|
|
35
40
|
def verify_signature
|
41
|
+
return unless @key || @verify
|
42
|
+
|
43
|
+
return if none_algorithm?
|
44
|
+
|
45
|
+
raise JWT::DecodeError, 'No verification key available' unless @key
|
46
|
+
|
47
|
+
return if Array(@key).any? { |key| verify_signature_for?(key) }
|
48
|
+
|
49
|
+
raise(JWT::VerificationError, 'Signature verification failed')
|
50
|
+
end
|
51
|
+
|
52
|
+
def verify_algo
|
36
53
|
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
54
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
|
37
55
|
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
56
|
+
end
|
38
57
|
|
58
|
+
def set_key
|
39
59
|
@key = find_key(&@keyfinder) if @keyfinder
|
40
60
|
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
61
|
+
if (x5c_options = @options[:x5c])
|
62
|
+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
|
63
|
+
end
|
64
|
+
end
|
41
65
|
|
42
|
-
|
66
|
+
def verify_signature_for?(key)
|
67
|
+
Signature.verify(algorithm, key, signing_input, @signature)
|
43
68
|
end
|
44
69
|
|
45
70
|
def options_includes_algo_in_header?
|
46
|
-
allowed_algorithms.any? { |alg| alg.casecmp(
|
71
|
+
allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
|
47
72
|
end
|
48
73
|
|
49
74
|
def allowed_algorithms
|
@@ -64,18 +89,21 @@ module JWT
|
|
64
89
|
|
65
90
|
def find_key(&keyfinder)
|
66
91
|
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
67
|
-
|
68
|
-
key
|
92
|
+
# key can be of type [string, nil, OpenSSL::PKey, Array]
|
93
|
+
return key if key && !Array(key).empty?
|
94
|
+
|
95
|
+
raise JWT::DecodeError, 'No verification key available'
|
69
96
|
end
|
70
97
|
|
71
98
|
def verify_claims
|
72
99
|
Verify.verify_claims(payload, @options)
|
100
|
+
Verify.verify_required_claims(payload, @options)
|
73
101
|
end
|
74
102
|
|
75
103
|
def validate_segment_count!
|
76
104
|
return if segment_length == 3
|
77
105
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
78
|
-
return if segment_length == 2 &&
|
106
|
+
return if segment_length == 2 && none_algorithm?
|
79
107
|
|
80
108
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
81
109
|
end
|
@@ -84,8 +112,16 @@ module JWT
|
|
84
112
|
@segments.count
|
85
113
|
end
|
86
114
|
|
115
|
+
def none_algorithm?
|
116
|
+
algorithm.casecmp('none').zero?
|
117
|
+
end
|
118
|
+
|
87
119
|
def decode_crypto
|
88
|
-
@signature =
|
120
|
+
@signature = Base64.urlsafe_decode64(@segments[2] || '')
|
121
|
+
end
|
122
|
+
|
123
|
+
def algorithm
|
124
|
+
header['alg']
|
89
125
|
end
|
90
126
|
|
91
127
|
def header
|
@@ -101,8 +137,8 @@ module JWT
|
|
101
137
|
end
|
102
138
|
|
103
139
|
def parse_and_decode(segment)
|
104
|
-
JWT::JSON.parse(
|
105
|
-
rescue ::JSON::ParserError
|
140
|
+
JWT::JSON.parse(Base64.urlsafe_decode64(segment))
|
141
|
+
rescue ::JSON::ParserError, ArgumentError
|
106
142
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
107
143
|
end
|
108
144
|
end
|
data/lib/jwt/default_options.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module DefaultOptions
|
3
5
|
DEFAULT_OPTIONS = {
|
@@ -9,7 +11,8 @@ module JWT
|
|
9
11
|
verify_aud: false,
|
10
12
|
verify_sub: false,
|
11
13
|
leeway: 0,
|
12
|
-
algorithms: ['HS256']
|
14
|
+
algorithms: ['HS256'],
|
15
|
+
required_claims: []
|
13
16
|
}.freeze
|
14
17
|
end
|
15
18
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -7,14 +7,14 @@ require_relative './claims_validator'
|
|
7
7
|
module JWT
|
8
8
|
# Encoding logic for JWT
|
9
9
|
class Encode
|
10
|
-
ALG_NONE = 'none'
|
11
|
-
ALG_KEY = 'alg'
|
10
|
+
ALG_NONE = 'none'
|
11
|
+
ALG_KEY = 'alg'
|
12
12
|
|
13
13
|
def initialize(options)
|
14
14
|
@payload = options[:payload]
|
15
15
|
@key = options[:key]
|
16
16
|
_, @algorithm = Algos.find(options[:algorithm])
|
17
|
-
@headers = options[:headers].
|
17
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
18
18
|
end
|
19
19
|
|
20
20
|
def segments
|
@@ -45,7 +45,7 @@ module JWT
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def encode_payload
|
48
|
-
if @payload
|
48
|
+
if @payload.is_a?(Hash)
|
49
49
|
ClaimsValidator.new(@payload).validate!
|
50
50
|
end
|
51
51
|
|
@@ -55,11 +55,11 @@ module JWT
|
|
55
55
|
def encode_signature
|
56
56
|
return '' if @algorithm == ALG_NONE
|
57
57
|
|
58
|
-
|
58
|
+
Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
|
59
59
|
end
|
60
60
|
|
61
61
|
def encode(data)
|
62
|
-
|
62
|
+
Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false)
|
63
63
|
end
|
64
64
|
|
65
65
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -10,11 +10,13 @@ module JWT
|
|
10
10
|
class IncorrectAlgorithm < DecodeError; end
|
11
11
|
class ImmatureSignature < DecodeError; end
|
12
12
|
class InvalidIssuerError < DecodeError; end
|
13
|
+
class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
|
13
14
|
class InvalidIatError < DecodeError; end
|
14
15
|
class InvalidAudError < DecodeError; end
|
15
16
|
class InvalidSubError < DecodeError; end
|
16
17
|
class InvalidJtiError < DecodeError; end
|
17
18
|
class InvalidPayload < DecodeError; end
|
19
|
+
class MissingRequiredClaim < DecodeError; end
|
18
20
|
|
19
21
|
class JWKError < DecodeError; end
|
20
22
|
end
|