jwt 2.3.0 → 2.10.1
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 +194 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +360 -106
- data/lib/jwt/base64.rb +19 -2
- data/lib/jwt/claims/audience.rb +30 -0
- data/lib/jwt/claims/crit.rb +35 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +32 -0
- data/lib/jwt/claims/issued_at.rb +22 -0
- data/lib/jwt/claims/issuer.rb +34 -0
- data/lib/jwt/claims/jwt_id.rb +35 -0
- data/lib/jwt/claims/not_before.rb +32 -0
- data/lib/jwt/claims/numeric.rb +77 -0
- data/lib/jwt/claims/required.rb +33 -0
- data/lib/jwt/claims/subject.rb +30 -0
- data/lib/jwt/claims/verification_methods.rb +20 -0
- data/lib/jwt/claims/verifier.rb +61 -0
- data/lib/jwt/claims.rb +74 -0
- data/lib/jwt/claims_validator.rb +7 -24
- data/lib/jwt/configuration/container.rb +52 -0
- data/lib/jwt/configuration/decode_configuration.rb +70 -0
- data/lib/jwt/configuration/jwk_configuration.rb +28 -0
- data/lib/jwt/configuration.rb +23 -0
- data/lib/jwt/decode.rb +70 -61
- data/lib/jwt/deprecations.rb +49 -0
- data/lib/jwt/encode.rb +18 -57
- data/lib/jwt/encoded_token.rb +139 -0
- data/lib/jwt/error.rb +36 -0
- data/lib/jwt/json.rb +1 -1
- data/lib/jwt/jwa/compat.rb +32 -0
- data/lib/jwt/jwa/ecdsa.rb +90 -0
- data/lib/jwt/jwa/eddsa.rb +35 -0
- data/lib/jwt/jwa/hmac.rb +82 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
- data/lib/jwt/jwa/none.rb +24 -0
- data/lib/jwt/jwa/ps.rb +35 -0
- data/lib/jwt/jwa/rsa.rb +35 -0
- data/lib/jwt/jwa/signing_algorithm.rb +63 -0
- data/lib/jwt/jwa/unsupported.rb +20 -0
- data/lib/jwt/jwa/wrapper.rb +44 -0
- data/lib/jwt/jwa.rb +58 -0
- data/lib/jwt/jwk/ec.rb +163 -63
- data/lib/jwt/jwk/hmac.rb +68 -24
- data/lib/jwt/jwk/key_base.rb +46 -6
- data/lib/jwt/jwk/key_finder.rb +20 -35
- data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
- data/lib/jwt/jwk/rsa.rb +141 -54
- data/lib/jwt/jwk/set.rb +82 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +16 -11
- data/lib/jwt/token.rb +112 -0
- data/lib/jwt/verify.rb +16 -81
- data/lib/jwt/version.rb +53 -11
- data/lib/jwt/x5c_key_finder.rb +52 -0
- data/lib/jwt.rb +28 -4
- data/ruby-jwt.gemspec +15 -5
- metadata +75 -28
- 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/algos/ecdsa.rb +0 -35
- data/lib/jwt/algos/eddsa.rb +0 -30
- data/lib/jwt/algos/hmac.rb +0 -34
- data/lib/jwt/algos/none.rb +0 -15
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -19
- data/lib/jwt/algos/unsupported.rb +0 -17
- data/lib/jwt/algos.rb +0 -44
- data/lib/jwt/default_options.rb +0 -16
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -39
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'decode_configuration'
|
4
|
+
require_relative 'jwk_configuration'
|
5
|
+
|
6
|
+
module JWT
|
7
|
+
module Configuration
|
8
|
+
# The Container class holds the configuration settings for JWT.
|
9
|
+
class Container
|
10
|
+
# @!attribute [rw] decode
|
11
|
+
# @return [DecodeConfiguration] the decode configuration.
|
12
|
+
# @!attribute [rw] jwk
|
13
|
+
# @return [JwkConfiguration] the JWK configuration.
|
14
|
+
# @!attribute [rw] strict_base64_decoding
|
15
|
+
# @return [Boolean] whether strict Base64 decoding is enabled.
|
16
|
+
attr_accessor :decode, :jwk, :strict_base64_decoding
|
17
|
+
|
18
|
+
# @!attribute [r] deprecation_warnings
|
19
|
+
# @return [Symbol] the deprecation warnings setting.
|
20
|
+
attr_reader :deprecation_warnings
|
21
|
+
|
22
|
+
# Initializes a new Container instance and resets the configuration.
|
23
|
+
def initialize
|
24
|
+
reset!
|
25
|
+
end
|
26
|
+
|
27
|
+
# Resets the configuration to default values.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def reset!
|
31
|
+
@decode = DecodeConfiguration.new
|
32
|
+
@jwk = JwkConfiguration.new
|
33
|
+
@strict_base64_decoding = false
|
34
|
+
|
35
|
+
self.deprecation_warnings = :once
|
36
|
+
end
|
37
|
+
|
38
|
+
DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze
|
39
|
+
private_constant(:DEPRECATION_WARNINGS_VALUES)
|
40
|
+
# Sets the deprecation warnings setting.
|
41
|
+
#
|
42
|
+
# @param value [Symbol] the deprecation warnings setting. Must be one of `:once`, `:warn`, or `:silent`.
|
43
|
+
# @raise [ArgumentError] if the value is not one of the supported values.
|
44
|
+
# @return [void]
|
45
|
+
def deprecation_warnings=(value)
|
46
|
+
raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value)
|
47
|
+
|
48
|
+
@deprecation_warnings = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Configuration
|
5
|
+
# The DecodeConfiguration class holds the configuration settings for decoding JWT tokens.
|
6
|
+
class DecodeConfiguration
|
7
|
+
# @!attribute [rw] verify_expiration
|
8
|
+
# @return [Boolean] whether to verify the expiration claim.
|
9
|
+
# @!attribute [rw] verify_not_before
|
10
|
+
# @return [Boolean] whether to verify the not before claim.
|
11
|
+
# @!attribute [rw] verify_iss
|
12
|
+
# @return [Boolean] whether to verify the issuer claim.
|
13
|
+
# @!attribute [rw] verify_iat
|
14
|
+
# @return [Boolean] whether to verify the issued at claim.
|
15
|
+
# @!attribute [rw] verify_jti
|
16
|
+
# @return [Boolean] whether to verify the JWT ID claim.
|
17
|
+
# @!attribute [rw] verify_aud
|
18
|
+
# @return [Boolean] whether to verify the audience claim.
|
19
|
+
# @!attribute [rw] verify_sub
|
20
|
+
# @return [Boolean] whether to verify the subject claim.
|
21
|
+
# @!attribute [rw] leeway
|
22
|
+
# @return [Integer] the leeway in seconds for time-based claims.
|
23
|
+
# @!attribute [rw] algorithms
|
24
|
+
# @return [Array<String>] the list of acceptable algorithms.
|
25
|
+
# @!attribute [rw] required_claims
|
26
|
+
# @return [Array<String>] the list of required claims.
|
27
|
+
|
28
|
+
attr_accessor :verify_expiration,
|
29
|
+
:verify_not_before,
|
30
|
+
:verify_iss,
|
31
|
+
:verify_iat,
|
32
|
+
:verify_jti,
|
33
|
+
:verify_aud,
|
34
|
+
:verify_sub,
|
35
|
+
:leeway,
|
36
|
+
:algorithms,
|
37
|
+
:required_claims
|
38
|
+
|
39
|
+
# Initializes a new DecodeConfiguration instance with default settings.
|
40
|
+
def initialize
|
41
|
+
@verify_expiration = true
|
42
|
+
@verify_not_before = true
|
43
|
+
@verify_iss = false
|
44
|
+
@verify_iat = false
|
45
|
+
@verify_jti = false
|
46
|
+
@verify_aud = false
|
47
|
+
@verify_sub = false
|
48
|
+
@leeway = 0
|
49
|
+
@algorithms = ['HS256']
|
50
|
+
@required_claims = []
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def to_h
|
55
|
+
{
|
56
|
+
verify_expiration: verify_expiration,
|
57
|
+
verify_not_before: verify_not_before,
|
58
|
+
verify_iss: verify_iss,
|
59
|
+
verify_iat: verify_iat,
|
60
|
+
verify_jti: verify_jti,
|
61
|
+
verify_aud: verify_aud,
|
62
|
+
verify_sub: verify_sub,
|
63
|
+
leeway: leeway,
|
64
|
+
algorithms: algorithms,
|
65
|
+
required_claims: required_claims
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../jwk/kid_as_key_digest'
|
4
|
+
require_relative '../jwk/thumbprint'
|
5
|
+
|
6
|
+
module JWT
|
7
|
+
module Configuration
|
8
|
+
# @api private
|
9
|
+
class JwkConfiguration
|
10
|
+
def initialize
|
11
|
+
self.kid_generator_type = :key_digest
|
12
|
+
end
|
13
|
+
|
14
|
+
def kid_generator_type=(value)
|
15
|
+
self.kid_generator = case value
|
16
|
+
when :key_digest
|
17
|
+
JWT::JWK::KidAsKeyDigest
|
18
|
+
when :rfc7638_thumbprint
|
19
|
+
JWT::JWK::Thumbprint
|
20
|
+
else
|
21
|
+
raise ArgumentError, "#{value} is not a valid kid generator type."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :kid_generator
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'configuration/container'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
# The Configuration module provides methods to configure JWT settings.
|
7
|
+
module Configuration
|
8
|
+
# Configures the JWT settings.
|
9
|
+
#
|
10
|
+
# @yield [config] Gives the current configuration to the block.
|
11
|
+
# @yieldparam config [JWT::Configuration::Container] the configuration container.
|
12
|
+
def configure
|
13
|
+
yield(configuration)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the JWT configuration container.
|
17
|
+
#
|
18
|
+
# @return [JWT::Configuration::Container] the configuration container.
|
19
|
+
def configuration
|
20
|
+
@configuration ||= ::JWT::Configuration::Container.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/jwt/decode.rb
CHANGED
@@ -1,111 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
+
require 'jwt/x5c_key_finder'
|
4
5
|
|
5
|
-
require 'jwt/signature'
|
6
|
-
require 'jwt/verify'
|
7
|
-
# JWT::Decode module
|
8
6
|
module JWT
|
9
|
-
#
|
7
|
+
# The Decode class is responsible for decoding and verifying JWT tokens.
|
10
8
|
class Decode
|
9
|
+
# Initializes a new Decode instance.
|
10
|
+
#
|
11
|
+
# @param jwt [String] the JWT to decode.
|
12
|
+
# @param key [String, Array<String>] the key(s) to use for verification.
|
13
|
+
# @param verify [Boolean] whether to verify the token's signature.
|
14
|
+
# @param options [Hash] additional options for decoding and verification.
|
15
|
+
# @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification.
|
16
|
+
# @raise [JWT::DecodeError] if decoding or verification fails.
|
11
17
|
def initialize(jwt, key, verify, options, &keyfinder)
|
12
|
-
raise
|
13
|
-
|
18
|
+
raise JWT::DecodeError, 'Nil JSON web token' unless jwt
|
19
|
+
|
20
|
+
@token = EncodedToken.new(jwt)
|
14
21
|
@key = key
|
15
22
|
@options = options
|
16
|
-
@segments = jwt.split('.')
|
17
23
|
@verify = verify
|
18
|
-
@signature = ''
|
19
24
|
@keyfinder = keyfinder
|
20
25
|
end
|
21
26
|
|
27
|
+
# Decodes the JWT token and verifies its segments if verification is enabled.
|
28
|
+
#
|
29
|
+
# @return [Array<Hash>] an array containing the decoded payload and header.
|
22
30
|
def decode_segments
|
23
31
|
validate_segment_count!
|
24
32
|
if @verify
|
25
|
-
|
33
|
+
verify_algo
|
34
|
+
set_key
|
26
35
|
verify_signature
|
27
|
-
|
36
|
+
Claims::DecodeVerifier.verify!(token.payload, @options)
|
28
37
|
end
|
29
|
-
|
30
|
-
[payload, header]
|
38
|
+
|
39
|
+
[token.payload, token.header]
|
31
40
|
end
|
32
41
|
|
33
42
|
private
|
34
43
|
|
44
|
+
attr_reader :token
|
45
|
+
|
35
46
|
def verify_signature
|
36
|
-
|
37
|
-
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
|
38
|
-
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
47
|
+
return if none_algorithm?
|
39
48
|
|
40
|
-
|
41
|
-
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
49
|
+
raise JWT::DecodeError, 'No verification key available' unless @key
|
42
50
|
|
43
|
-
|
51
|
+
token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key)
|
44
52
|
end
|
45
53
|
|
46
|
-
def
|
47
|
-
|
54
|
+
def verify_algo
|
55
|
+
raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
|
56
|
+
raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash)
|
57
|
+
raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
|
58
|
+
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
|
48
59
|
end
|
49
60
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
elsif @options.key?(:algorithm)
|
55
|
-
@options[:algorithm]
|
56
|
-
elsif @options.key?('algorithms')
|
57
|
-
@options['algorithms']
|
58
|
-
elsif @options.key?(:algorithms)
|
59
|
-
@options[:algorithms]
|
60
|
-
else
|
61
|
-
[]
|
62
|
-
end
|
63
|
-
Array(algos)
|
64
|
-
end
|
61
|
+
def set_key
|
62
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
63
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
|
64
|
+
return unless (x5c_options = @options[:x5c])
|
65
65
|
|
66
|
-
|
67
|
-
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
68
|
-
raise JWT::DecodeError, 'No verification key available' unless key
|
69
|
-
key
|
66
|
+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
|
70
67
|
end
|
71
68
|
|
72
|
-
def
|
73
|
-
|
74
|
-
Verify.verify_required_claims(payload, @options)
|
69
|
+
def allowed_and_valid_algorithms
|
70
|
+
@allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
|
75
71
|
end
|
76
72
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
73
|
+
# Order is very important - first check for string keys, next for symbols
|
74
|
+
ALGORITHM_KEYS = ['algorithm',
|
75
|
+
:algorithm,
|
76
|
+
'algorithms',
|
77
|
+
:algorithms].freeze
|
81
78
|
|
82
|
-
|
79
|
+
def given_algorithms
|
80
|
+
ALGORITHM_KEYS.each do |alg_key|
|
81
|
+
alg = @options[alg_key]
|
82
|
+
return Array(alg) if alg
|
83
|
+
end
|
84
|
+
[]
|
83
85
|
end
|
84
86
|
|
85
|
-
def
|
86
|
-
@
|
87
|
+
def allowed_algorithms
|
88
|
+
@allowed_algorithms ||= resolve_allowed_algorithms
|
87
89
|
end
|
88
90
|
|
89
|
-
def
|
90
|
-
|
91
|
+
def resolve_allowed_algorithms
|
92
|
+
given_algorithms.map { |alg| JWA.resolve(alg) }
|
91
93
|
end
|
92
94
|
|
93
|
-
def
|
94
|
-
|
95
|
+
def find_key(&keyfinder)
|
96
|
+
key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.header))
|
97
|
+
# key can be of type [string, nil, OpenSSL::PKey, Array]
|
98
|
+
return key if key && !Array(key).empty?
|
99
|
+
|
100
|
+
raise JWT::DecodeError, 'No verification key available'
|
95
101
|
end
|
96
102
|
|
97
|
-
def
|
98
|
-
|
103
|
+
def validate_segment_count!
|
104
|
+
segment_count = token.jwt.count('.') + 1
|
105
|
+
return if segment_count == 3
|
106
|
+
return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed
|
107
|
+
return if segment_count == 2 && none_algorithm?
|
108
|
+
|
109
|
+
raise JWT::DecodeError, 'Not enough or too many segments'
|
99
110
|
end
|
100
111
|
|
101
|
-
def
|
102
|
-
|
112
|
+
def none_algorithm?
|
113
|
+
alg_in_header == 'none'
|
103
114
|
end
|
104
115
|
|
105
|
-
def
|
106
|
-
|
107
|
-
rescue ::JSON::ParserError
|
108
|
-
raise JWT::DecodeError, 'Invalid segment encoding'
|
116
|
+
def alg_in_header
|
117
|
+
token.header['alg']
|
109
118
|
end
|
110
119
|
end
|
111
120
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
# Deprecations module to handle deprecation warnings in the gem
|
5
|
+
# @api private
|
6
|
+
module Deprecations
|
7
|
+
class << self
|
8
|
+
def context
|
9
|
+
yield.tap { emit_warnings }
|
10
|
+
ensure
|
11
|
+
Thread.current[:jwt_warning_store] = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def warning(message, only_if_valid: false)
|
15
|
+
method_name = only_if_valid ? :store : :warn
|
16
|
+
case JWT.configuration.deprecation_warnings
|
17
|
+
when :once
|
18
|
+
return if record_warned(message)
|
19
|
+
when :warn
|
20
|
+
# noop
|
21
|
+
else
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
send(method_name, "[DEPRECATION WARNING] #{message}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def store(message)
|
29
|
+
(Thread.current[:jwt_warning_store] ||= []) << message
|
30
|
+
end
|
31
|
+
|
32
|
+
def emit_warnings
|
33
|
+
return if Thread.current[:jwt_warning_store].nil?
|
34
|
+
|
35
|
+
Thread.current[:jwt_warning_store].each { |warning| warn(warning) }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def record_warned(message)
|
41
|
+
@warned ||= []
|
42
|
+
return true if @warned.include?(message)
|
43
|
+
|
44
|
+
@warned << message
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,69 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative './claims_validator'
|
3
|
+
require_relative 'jwa'
|
5
4
|
|
6
|
-
# JWT::Encode module
|
7
5
|
module JWT
|
8
|
-
#
|
6
|
+
# The Encode class is responsible for encoding JWT tokens.
|
9
7
|
class Encode
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
# Initializes a new Encode instance.
|
9
|
+
#
|
10
|
+
# @param options [Hash] the options for encoding the JWT token.
|
11
|
+
# @option options [Hash] :payload the payload of the JWT token.
|
12
|
+
# @option options [Hash] :headers the headers of the JWT token.
|
13
|
+
# @option options [String] :key the key used to sign the JWT token.
|
14
|
+
# @option options [String] :algorithm the algorithm used to sign the JWT token.
|
13
15
|
def initialize(options)
|
14
|
-
@payload
|
15
|
-
@key
|
16
|
-
|
17
|
-
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
|
16
|
+
@token = Token.new(payload: options[:payload], header: options[:headers])
|
17
|
+
@key = options[:key]
|
18
|
+
@algorithm = options[:algorithm]
|
18
19
|
end
|
19
20
|
|
21
|
+
# Encodes the JWT token and returns its segments.
|
22
|
+
#
|
23
|
+
# @return [String] the encoded JWT token.
|
20
24
|
def segments
|
21
|
-
@
|
22
|
-
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def encoded_header
|
27
|
-
@encoded_header ||= encode_header
|
28
|
-
end
|
29
|
-
|
30
|
-
def encoded_payload
|
31
|
-
@encoded_payload ||= encode_payload
|
32
|
-
end
|
33
|
-
|
34
|
-
def encoded_signature
|
35
|
-
@encoded_signature ||= encode_signature
|
36
|
-
end
|
37
|
-
|
38
|
-
def encoded_header_and_payload
|
39
|
-
@encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
|
40
|
-
end
|
41
|
-
|
42
|
-
def encode_header
|
43
|
-
@headers[ALG_KEY] = @algorithm
|
44
|
-
encode(@headers)
|
45
|
-
end
|
46
|
-
|
47
|
-
def encode_payload
|
48
|
-
if @payload && @payload.is_a?(Hash)
|
49
|
-
ClaimsValidator.new(@payload).validate!
|
50
|
-
end
|
51
|
-
|
52
|
-
encode(@payload)
|
53
|
-
end
|
54
|
-
|
55
|
-
def encode_signature
|
56
|
-
return '' if @algorithm == ALG_NONE
|
57
|
-
|
58
|
-
JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
|
59
|
-
end
|
60
|
-
|
61
|
-
def encode(data)
|
62
|
-
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
63
|
-
end
|
64
|
-
|
65
|
-
def combine(*parts)
|
66
|
-
parts.join('.')
|
25
|
+
@token.verify_claims!(:numeric)
|
26
|
+
@token.sign!(algorithm: @algorithm, key: @key)
|
27
|
+
@token.jwt
|
67
28
|
end
|
68
29
|
end
|
69
30
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
# Represents an encoded JWT token
|
5
|
+
#
|
6
|
+
# Processing an encoded and signed token:
|
7
|
+
#
|
8
|
+
# token = JWT::Token.new(payload: {pay: 'load'})
|
9
|
+
# token.sign!(algorithm: 'HS256', key: 'secret')
|
10
|
+
#
|
11
|
+
# encoded_token = JWT::EncodedToken.new(token.jwt)
|
12
|
+
# encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret')
|
13
|
+
# encoded_token.payload # => {'pay' => 'load'}
|
14
|
+
class EncodedToken
|
15
|
+
include Claims::VerificationMethods
|
16
|
+
|
17
|
+
# Returns the original token provided to the class.
|
18
|
+
# @return [String] The JWT token.
|
19
|
+
attr_reader :jwt
|
20
|
+
|
21
|
+
# Initializes a new EncodedToken instance.
|
22
|
+
#
|
23
|
+
# @param jwt [String] the encoded JWT token.
|
24
|
+
# @raise [ArgumentError] if the provided JWT is not a String.
|
25
|
+
def initialize(jwt)
|
26
|
+
raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)
|
27
|
+
|
28
|
+
@jwt = jwt
|
29
|
+
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the decoded signature of the JWT token.
|
33
|
+
#
|
34
|
+
# @return [String] the decoded signature.
|
35
|
+
def signature
|
36
|
+
@signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the encoded signature of the JWT token.
|
40
|
+
#
|
41
|
+
# @return [String] the encoded signature.
|
42
|
+
attr_reader :encoded_signature
|
43
|
+
|
44
|
+
# Returns the decoded header of the JWT token.
|
45
|
+
#
|
46
|
+
# @return [Hash] the header.
|
47
|
+
def header
|
48
|
+
@header ||= parse_and_decode(@encoded_header)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the encoded header of the JWT token.
|
52
|
+
#
|
53
|
+
# @return [String] the encoded header.
|
54
|
+
attr_reader :encoded_header
|
55
|
+
|
56
|
+
# Returns the payload of the JWT token.
|
57
|
+
#
|
58
|
+
# @return [Hash] the payload.
|
59
|
+
def payload
|
60
|
+
@payload ||= decode_payload
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sets or returns the encoded payload of the JWT token.
|
64
|
+
#
|
65
|
+
# @return [String] the encoded payload.
|
66
|
+
attr_accessor :encoded_payload
|
67
|
+
|
68
|
+
# Returns the signing input of the JWT token.
|
69
|
+
#
|
70
|
+
# @return [String] the signing input.
|
71
|
+
def signing_input
|
72
|
+
[encoded_header, encoded_payload].join('.')
|
73
|
+
end
|
74
|
+
|
75
|
+
# Verifies the signature of the JWT token.
|
76
|
+
#
|
77
|
+
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
|
78
|
+
# @param key [String, Array<String>] the key(s) to use for verification.
|
79
|
+
# @param key_finder [#call] an object responding to `call` to find the key for verification.
|
80
|
+
# @return [nil]
|
81
|
+
# @raise [JWT::VerificationError] if the signature verification fails.
|
82
|
+
# @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
|
83
|
+
def verify_signature!(algorithm:, key: nil, key_finder: nil)
|
84
|
+
raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
|
85
|
+
|
86
|
+
key ||= key_finder.call(self)
|
87
|
+
|
88
|
+
return if valid_signature?(algorithm: algorithm, key: key)
|
89
|
+
|
90
|
+
raise JWT::VerificationError, 'Signature verification failed'
|
91
|
+
end
|
92
|
+
|
93
|
+
# Checks if the signature of the JWT token is valid.
|
94
|
+
#
|
95
|
+
# @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
|
96
|
+
# @param key [String, Array<String>] the key(s) to use for verification.
|
97
|
+
# @return [Boolean] true if the signature is valid, false otherwise.
|
98
|
+
def valid_signature?(algorithm:, key:)
|
99
|
+
Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
|
100
|
+
Array(key).any? do |one_key|
|
101
|
+
algo.verify(data: signing_input, signature: signature, verification_key: one_key)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
alias to_s jwt
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def decode_payload
|
111
|
+
raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
|
112
|
+
|
113
|
+
if unencoded_payload?
|
114
|
+
verify_claims!(crit: ['b64'])
|
115
|
+
return parse_unencoded(encoded_payload)
|
116
|
+
end
|
117
|
+
|
118
|
+
parse_and_decode(encoded_payload)
|
119
|
+
end
|
120
|
+
|
121
|
+
def unencoded_payload?
|
122
|
+
header['b64'] == false
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_and_decode(segment)
|
126
|
+
parse(::JWT::Base64.url_decode(segment || ''))
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_unencoded(segment)
|
130
|
+
parse(segment)
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse(segment)
|
134
|
+
JWT::JSON.parse(segment)
|
135
|
+
rescue ::JSON::ParserError
|
136
|
+
raise JWT::DecodeError, 'Invalid segment encoding'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|