jwt 2.2.1 → 2.3.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/.github/workflows/test.yml +74 -0
- data/.rspec +1 -0
- data/.rubocop.yml +15 -16
- data/.rubocop_todo.yml +185 -0
- data/{.ebert.yml → .sourcelevel.yml} +1 -1
- data/AUTHORS +60 -32
- data/Appraisals +4 -8
- data/CHANGELOG.md +189 -20
- data/Gemfile +2 -0
- data/README.md +78 -8
- data/Rakefile +4 -1
- data/lib/jwt/algos/eddsa.rb +11 -4
- data/lib/jwt/algos/hmac.rb +1 -0
- data/lib/jwt/algos/none.rb +15 -0
- data/lib/jwt/algos/unsupported.rb +5 -4
- data/lib/jwt/algos.rb +44 -0
- data/lib/jwt/claims_validator.rb +9 -7
- data/lib/jwt/decode.rb +19 -8
- data/lib/jwt/default_options.rb +2 -1
- data/lib/jwt/encode.rb +5 -4
- data/lib/jwt/error.rb +15 -14
- data/lib/jwt/jwk/ec.rb +150 -0
- data/lib/jwt/jwk/hmac.rb +58 -0
- data/lib/jwt/jwk/key_base.rb +18 -0
- data/lib/jwt/jwk/key_finder.rb +6 -1
- data/lib/jwt/jwk/rsa.rb +93 -25
- data/lib/jwt/jwk.rb +31 -11
- data/lib/jwt/signature.rb +9 -22
- data/lib/jwt/verify.rb +8 -1
- data/lib/jwt/version.rb +3 -3
- data/ruby-jwt.gemspec +4 -6
- metadata +17 -80
- data/.codeclimate.yml +0 -20
- data/.travis.yml +0 -20
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
9
|
|
|
10
10
|
A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
|
|
11
11
|
|
|
@@ -16,11 +16,17 @@ If you have further questions related to development or usage, join us: [ruby-jw
|
|
|
16
16
|
* Ruby 1.9.3 support was dropped at December 31st, 2016.
|
|
17
17
|
* 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
18
|
|
|
19
|
+
## Sponsors
|
|
20
|
+
|
|
21
|
+
|Logo|Message|
|
|
22
|
+
|-|-|
|
|
23
|
+
||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)|
|
|
24
|
+
|
|
19
25
|
## Installing
|
|
20
26
|
|
|
21
27
|
### Using Rubygems:
|
|
22
28
|
```bash
|
|
23
|
-
|
|
29
|
+
gem install jwt
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
### Using Bundler:
|
|
@@ -32,7 +38,7 @@ And run `bundle install`
|
|
|
32
38
|
|
|
33
39
|
## Algorithms and Usage
|
|
34
40
|
|
|
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/
|
|
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**
|
|
36
42
|
|
|
37
43
|
See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
|
|
38
44
|
|
|
@@ -70,6 +76,7 @@ puts decoded_token
|
|
|
70
76
|
* HS512 - HMAC using SHA-512 hash algorithm
|
|
71
77
|
|
|
72
78
|
```ruby
|
|
79
|
+
# The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
|
|
73
80
|
hmac_secret = 'my$ecretK3y'
|
|
74
81
|
|
|
75
82
|
token = JWT.encode payload, hmac_secret, 'HS256'
|
|
@@ -270,6 +277,12 @@ rescue JWT::ExpiredSignature
|
|
|
270
277
|
end
|
|
271
278
|
```
|
|
272
279
|
|
|
280
|
+
The Expiration Claim verification can be disabled.
|
|
281
|
+
```ruby
|
|
282
|
+
# Decode token without raising JWT::ExpiredSignature error
|
|
283
|
+
JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
|
|
284
|
+
```
|
|
285
|
+
|
|
273
286
|
**Adding Leeway**
|
|
274
287
|
|
|
275
288
|
```ruby
|
|
@@ -310,6 +323,12 @@ rescue JWT::ImmatureSignature
|
|
|
310
323
|
end
|
|
311
324
|
```
|
|
312
325
|
|
|
326
|
+
The Not Before Claim verification can be disabled.
|
|
327
|
+
```ruby
|
|
328
|
+
# Decode token without raising JWT::ImmatureSignature error
|
|
329
|
+
JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
|
|
330
|
+
```
|
|
331
|
+
|
|
313
332
|
**Adding Leeway**
|
|
314
333
|
|
|
315
334
|
```ruby
|
|
@@ -391,6 +410,8 @@ begin
|
|
|
391
410
|
#decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
|
|
392
411
|
# Alternatively, pass a proc with your own code to check if the JTI has already been used
|
|
393
412
|
decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
|
|
413
|
+
# or
|
|
414
|
+
decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
|
|
394
415
|
rescue JWT::InvalidJtiError
|
|
395
416
|
# Handle invalid token, e.g. logout user or deny access
|
|
396
417
|
puts 'Error'
|
|
@@ -439,12 +460,42 @@ rescue JWT::InvalidSubError
|
|
|
439
460
|
end
|
|
440
461
|
```
|
|
441
462
|
|
|
463
|
+
### Finding a Key
|
|
464
|
+
|
|
465
|
+
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.
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
|
|
469
|
+
iss_payload = { data: 'data', iss: issuers.first }
|
|
470
|
+
|
|
471
|
+
secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
|
|
472
|
+
|
|
473
|
+
token = JWT.encode iss_payload, hmac_secret, 'HS256'
|
|
474
|
+
|
|
475
|
+
begin
|
|
476
|
+
# Add iss to the validation to check if the token has been manipulated
|
|
477
|
+
decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
|
|
478
|
+
secrets[payload['iss']]
|
|
479
|
+
end
|
|
480
|
+
rescue JWT::InvalidIssuerError
|
|
481
|
+
# Handle invalid token, e.g. logout user or deny access
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Required Claims
|
|
486
|
+
|
|
487
|
+
You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
|
|
488
|
+
```ruby
|
|
489
|
+
# Will raise a JWT::ExpiredSignature error if the 'exp' claim is absent
|
|
490
|
+
JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
|
|
491
|
+
```
|
|
492
|
+
|
|
442
493
|
### JSON Web Key (JWK)
|
|
443
494
|
|
|
444
495
|
JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
|
|
445
496
|
|
|
446
497
|
```ruby
|
|
447
|
-
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
|
498
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
|
|
448
499
|
payload, headers = { data: 'data' }, { kid: jwk.kid }
|
|
449
500
|
|
|
450
501
|
token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
|
|
@@ -460,10 +511,28 @@ begin
|
|
|
460
511
|
rescue JWT::JWKError
|
|
461
512
|
# Handle problems with the provided JWKs
|
|
462
513
|
rescue JWT::DecodeError
|
|
463
|
-
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
|
|
514
|
+
# Handle other decode related issues e.g. no kid in header, no matching public key found etc.
|
|
464
515
|
end
|
|
465
516
|
```
|
|
466
517
|
|
|
518
|
+
or by passing JWK as a simple Hash
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
|
|
522
|
+
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Importing and exporting JSON Web Keys
|
|
526
|
+
|
|
527
|
+
The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
|
531
|
+
|
|
532
|
+
jwk_hash = jwk.export
|
|
533
|
+
jwk_hash_with_private_key = jwk.export(include_private: true)
|
|
534
|
+
```
|
|
535
|
+
|
|
467
536
|
# Development and Tests
|
|
468
537
|
|
|
469
538
|
We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
|
|
@@ -472,10 +541,11 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
|
|
|
472
541
|
rake release
|
|
473
542
|
```
|
|
474
543
|
|
|
475
|
-
The tests are written with rspec.
|
|
544
|
+
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
545
|
|
|
477
546
|
```bash
|
|
478
|
-
bundle
|
|
547
|
+
bundle install
|
|
548
|
+
bundle exec appraisal rake test
|
|
479
549
|
```
|
|
480
550
|
|
|
481
551
|
**If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
require 'bundler/setup'
|
|
1
2
|
require 'bundler/gem_tasks'
|
|
2
3
|
|
|
3
4
|
begin
|
|
4
5
|
require 'rspec/core/rake_task'
|
|
6
|
+
require 'rubocop/rake_task'
|
|
5
7
|
|
|
6
8
|
RSpec::Core::RakeTask.new(:test)
|
|
9
|
+
RuboCop::RakeTask.new(:rubocop)
|
|
7
10
|
|
|
8
|
-
task default:
|
|
11
|
+
task default: %i[rubocop test]
|
|
9
12
|
rescue LoadError
|
|
10
13
|
puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
|
|
11
14
|
end
|
data/lib/jwt/algos/eddsa.rb
CHANGED
|
@@ -3,18 +3,25 @@ module JWT
|
|
|
3
3
|
module Eddsa
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
SUPPORTED = %w[ED25519].freeze
|
|
6
|
+
SUPPORTED = %w[ED25519 EdDSA].freeze
|
|
7
7
|
|
|
8
8
|
def sign(to_sign)
|
|
9
9
|
algorithm, msg, key = to_sign.values
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if key.class != RbNaCl::Signatures::Ed25519::SigningKey
|
|
11
|
+
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
|
|
12
|
+
end
|
|
13
|
+
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
|
14
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
key.sign(msg)
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def verify(to_verify)
|
|
16
21
|
algorithm, public_key, signing_input, signature = to_verify.values
|
|
17
|
-
|
|
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
|
|
19
26
|
public_key.verify(signature, signing_input)
|
|
20
27
|
end
|
data/lib/jwt/algos/hmac.rb
CHANGED
|
@@ -3,14 +3,15 @@ module JWT
|
|
|
3
3
|
module Unsupported
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
SUPPORTED =
|
|
7
|
-
def verify(*)
|
|
8
|
-
raise JWT::VerificationError, 'Algorithm not supported'
|
|
9
|
-
end
|
|
6
|
+
SUPPORTED = [].freeze
|
|
10
7
|
|
|
11
8
|
def sign(*)
|
|
12
9
|
raise NotImplementedError, 'Unsupported signing method'
|
|
13
10
|
end
|
|
11
|
+
|
|
12
|
+
def verify(*)
|
|
13
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
|
14
|
+
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
data/lib/jwt/algos.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt/algos/hmac'
|
|
4
|
+
require 'jwt/algos/eddsa'
|
|
5
|
+
require 'jwt/algos/ecdsa'
|
|
6
|
+
require 'jwt/algos/rsa'
|
|
7
|
+
require 'jwt/algos/ps'
|
|
8
|
+
require 'jwt/algos/none'
|
|
9
|
+
require 'jwt/algos/unsupported'
|
|
10
|
+
|
|
11
|
+
# JWT::Signature module
|
|
12
|
+
module JWT
|
|
13
|
+
# Signature logic for JWT
|
|
14
|
+
module Algos
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
ALGOS = [
|
|
18
|
+
Algos::Hmac,
|
|
19
|
+
Algos::Ecdsa,
|
|
20
|
+
Algos::Rsa,
|
|
21
|
+
Algos::Eddsa,
|
|
22
|
+
Algos::Ps,
|
|
23
|
+
Algos::None,
|
|
24
|
+
Algos::Unsupported
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def find(algorithm)
|
|
28
|
+
indexed[algorithm && algorithm.downcase]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def indexed
|
|
34
|
+
@indexed ||= begin
|
|
35
|
+
fallback = [Algos::Unsupported, nil]
|
|
36
|
+
ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
|
|
37
|
+
alg.const_get(:SUPPORTED).each do |code|
|
|
38
|
+
hash[code.downcase] = [alg, code]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/jwt/claims_validator.rb
CHANGED
|
@@ -2,7 +2,7 @@ require_relative './error'
|
|
|
2
2
|
|
|
3
3
|
module JWT
|
|
4
4
|
class ClaimsValidator
|
|
5
|
-
|
|
5
|
+
NUMERIC_CLAIMS = %i[
|
|
6
6
|
exp
|
|
7
7
|
iat
|
|
8
8
|
nbf
|
|
@@ -13,21 +13,23 @@ module JWT
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def validate!
|
|
16
|
-
|
|
16
|
+
validate_numeric_claims
|
|
17
17
|
|
|
18
18
|
true
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
def validate_numeric_claims
|
|
24
|
+
NUMERIC_CLAIMS.each do |claim|
|
|
25
|
+
validate_is_numeric(claim) if @payload.key?(claim)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
|
|
29
|
+
def validate_is_numeric(claim)
|
|
30
|
+
return if @payload[claim].is_a?(Numeric)
|
|
31
|
+
|
|
32
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
end
|
data/lib/jwt/decode.rb
CHANGED
|
@@ -33,25 +33,34 @@ module JWT
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def verify_signature
|
|
36
|
-
@key = find_key(&@keyfinder) if @keyfinder
|
|
37
|
-
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
|
38
|
-
|
|
39
36
|
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
|
37
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
|
|
40
38
|
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
|
41
39
|
|
|
40
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
|
41
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
|
42
|
+
|
|
42
43
|
Signature.verify(header['alg'], @key, signing_input, @signature)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def options_includes_algo_in_header?
|
|
46
|
-
allowed_algorithms.
|
|
47
|
+
allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def allowed_algorithms
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# Order is very important - first check for string keys, next for symbols
|
|
52
|
+
algos = if @options.key?('algorithm')
|
|
53
|
+
@options['algorithm']
|
|
54
|
+
elsif @options.key?(:algorithm)
|
|
55
|
+
@options[:algorithm]
|
|
56
|
+
elsif @options.key?('algorithms')
|
|
57
|
+
@options['algorithms']
|
|
58
|
+
elsif @options.key?(:algorithms)
|
|
59
|
+
@options[:algorithms]
|
|
52
60
|
else
|
|
53
|
-
|
|
61
|
+
[]
|
|
54
62
|
end
|
|
63
|
+
Array(algos)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
def find_key(&keyfinder)
|
|
@@ -62,11 +71,13 @@ module JWT
|
|
|
62
71
|
|
|
63
72
|
def verify_claims
|
|
64
73
|
Verify.verify_claims(payload, @options)
|
|
74
|
+
Verify.verify_required_claims(payload, @options)
|
|
65
75
|
end
|
|
66
76
|
|
|
67
77
|
def validate_segment_count!
|
|
68
78
|
return if segment_length == 3
|
|
69
79
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
|
80
|
+
return if segment_length == 2 && header['alg'] == 'none'
|
|
70
81
|
|
|
71
82
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
|
72
83
|
end
|
|
@@ -76,7 +87,7 @@ module JWT
|
|
|
76
87
|
end
|
|
77
88
|
|
|
78
89
|
def decode_crypto
|
|
79
|
-
@signature = JWT::Base64.url_decode(@segments[2])
|
|
90
|
+
@signature = JWT::Base64.url_decode(@segments[2] || '')
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
def header
|
data/lib/jwt/default_options.rb
CHANGED
data/lib/jwt/encode.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative './algos'
|
|
3
4
|
require_relative './claims_validator'
|
|
4
5
|
|
|
5
6
|
# JWT::Encode module
|
|
@@ -10,10 +11,10 @@ module JWT
|
|
|
10
11
|
ALG_KEY = 'alg'.freeze
|
|
11
12
|
|
|
12
13
|
def initialize(options)
|
|
13
|
-
@payload
|
|
14
|
-
@key
|
|
15
|
-
@algorithm = options[:algorithm]
|
|
16
|
-
@headers
|
|
14
|
+
@payload = options[:payload]
|
|
15
|
+
@key = options[:key]
|
|
16
|
+
_, @algorithm = Algos.find(options[:algorithm])
|
|
17
|
+
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def segments
|
data/lib/jwt/error.rb
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module JWT
|
|
4
|
-
EncodeError
|
|
5
|
-
DecodeError
|
|
6
|
-
RequiredDependencyError
|
|
4
|
+
class EncodeError < StandardError; end
|
|
5
|
+
class DecodeError < StandardError; end
|
|
6
|
+
class RequiredDependencyError < StandardError; end
|
|
7
7
|
|
|
8
|
-
VerificationError
|
|
9
|
-
ExpiredSignature
|
|
10
|
-
IncorrectAlgorithm
|
|
11
|
-
ImmatureSignature
|
|
12
|
-
InvalidIssuerError
|
|
13
|
-
InvalidIatError
|
|
14
|
-
InvalidAudError
|
|
15
|
-
InvalidSubError
|
|
16
|
-
InvalidJtiError
|
|
17
|
-
InvalidPayload
|
|
8
|
+
class VerificationError < DecodeError; end
|
|
9
|
+
class ExpiredSignature < DecodeError; end
|
|
10
|
+
class IncorrectAlgorithm < DecodeError; end
|
|
11
|
+
class ImmatureSignature < DecodeError; end
|
|
12
|
+
class InvalidIssuerError < DecodeError; end
|
|
13
|
+
class InvalidIatError < DecodeError; end
|
|
14
|
+
class InvalidAudError < DecodeError; end
|
|
15
|
+
class InvalidSubError < DecodeError; end
|
|
16
|
+
class InvalidJtiError < DecodeError; end
|
|
17
|
+
class InvalidPayload < DecodeError; end
|
|
18
|
+
class MissingRequiredClaim < DecodeError; end
|
|
18
19
|
|
|
19
|
-
JWKError
|
|
20
|
+
class JWKError < DecodeError; end
|
|
20
21
|
end
|
data/lib/jwt/jwk/ec.rb
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module JWT
|
|
6
|
+
module JWK
|
|
7
|
+
class EC < KeyBase
|
|
8
|
+
extend Forwardable
|
|
9
|
+
def_delegators :@keypair, :public_key
|
|
10
|
+
|
|
11
|
+
KTY = 'EC'.freeze
|
|
12
|
+
KTYS = [KTY, OpenSSL::PKey::EC].freeze
|
|
13
|
+
BINARY = 2
|
|
14
|
+
|
|
15
|
+
def initialize(keypair, kid = nil)
|
|
16
|
+
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
|
|
17
|
+
|
|
18
|
+
kid ||= generate_kid(keypair)
|
|
19
|
+
super(keypair, kid)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def private?
|
|
23
|
+
@keypair.private_key?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def export(options = {})
|
|
27
|
+
crv, x_octets, y_octets = keypair_components(keypair)
|
|
28
|
+
exported_hash = {
|
|
29
|
+
kty: KTY,
|
|
30
|
+
crv: crv,
|
|
31
|
+
x: encode_octets(x_octets),
|
|
32
|
+
y: encode_octets(y_octets),
|
|
33
|
+
kid: kid
|
|
34
|
+
}
|
|
35
|
+
return exported_hash unless private? && options[:include_private] == true
|
|
36
|
+
|
|
37
|
+
append_private_parts(exported_hash)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def append_private_parts(the_hash)
|
|
43
|
+
octets = keypair.private_key.to_bn.to_s(BINARY)
|
|
44
|
+
the_hash.merge(
|
|
45
|
+
d: encode_octets(octets)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def generate_kid(ec_keypair)
|
|
50
|
+
_crv, x_octets, y_octets = keypair_components(ec_keypair)
|
|
51
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
|
52
|
+
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
|
53
|
+
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def keypair_components(ec_keypair)
|
|
57
|
+
encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
|
|
58
|
+
case ec_keypair.group.curve_name
|
|
59
|
+
when 'prime256v1'
|
|
60
|
+
crv = 'P-256'
|
|
61
|
+
x_octets, y_octets = encoded_point.unpack('xa32a32')
|
|
62
|
+
when 'secp384r1'
|
|
63
|
+
crv = 'P-384'
|
|
64
|
+
x_octets, y_octets = encoded_point.unpack('xa48a48')
|
|
65
|
+
when 'secp521r1'
|
|
66
|
+
crv = 'P-521'
|
|
67
|
+
x_octets, y_octets = encoded_point.unpack('xa66a66')
|
|
68
|
+
else
|
|
69
|
+
raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
|
|
70
|
+
end
|
|
71
|
+
[crv, x_octets, y_octets]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def encode_octets(octets)
|
|
75
|
+
::JWT::Base64.url_encode(octets)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def encode_open_ssl_bn(key_part)
|
|
79
|
+
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class << self
|
|
83
|
+
def import(jwk_data)
|
|
84
|
+
# See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
|
|
85
|
+
# explanation of the relevant parameters.
|
|
86
|
+
|
|
87
|
+
jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
|
|
88
|
+
raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
|
|
89
|
+
|
|
90
|
+
new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_openssl_curve(crv)
|
|
94
|
+
# The JWK specs and OpenSSL use different names for the same curves.
|
|
95
|
+
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
|
96
|
+
# pointers on different names for common curves.
|
|
97
|
+
case crv
|
|
98
|
+
when 'P-256' then 'prime256v1'
|
|
99
|
+
when 'P-384' then 'secp384r1'
|
|
100
|
+
when 'P-521' then 'secp521r1'
|
|
101
|
+
else raise JWT::JWKError, 'Invalid curve provided'
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def jwk_attrs(jwk_data, attrs)
|
|
108
|
+
attrs.map do |attr|
|
|
109
|
+
jwk_data[attr] || jwk_data[attr.to_s]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
|
|
114
|
+
curve = to_openssl_curve(jwk_crv)
|
|
115
|
+
|
|
116
|
+
x_octets = decode_octets(jwk_x)
|
|
117
|
+
y_octets = decode_octets(jwk_y)
|
|
118
|
+
|
|
119
|
+
key = OpenSSL::PKey::EC.new(curve)
|
|
120
|
+
|
|
121
|
+
# The details of the `Point` instantiation are covered in:
|
|
122
|
+
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
|
|
123
|
+
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
|
|
124
|
+
# - https://tools.ietf.org/html/rfc5480#section-2.2
|
|
125
|
+
# - https://www.secg.org/SEC1-Ver-1.0.pdf
|
|
126
|
+
# Section 2.3.3 of the last of these references specifies that the
|
|
127
|
+
# encoding of an uncompressed point consists of the byte `0x04` followed
|
|
128
|
+
# by the x value then the y value.
|
|
129
|
+
point = OpenSSL::PKey::EC::Point.new(
|
|
130
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
|
131
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
key.public_key = point
|
|
135
|
+
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
|
|
136
|
+
|
|
137
|
+
key
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def decode_octets(jwk_data)
|
|
141
|
+
::JWT::Base64.url_decode(jwk_data)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def decode_open_ssl_bn(jwk_data)
|
|
145
|
+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|