Skip to content

Commit

Permalink
Add Support to be able to verify from multiple keys
Browse files Browse the repository at this point in the history
  • Loading branch information
ritikesh authored and anakinj committed Dec 13, 2021
1 parent d2a5f4b commit c4aa448
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -9,3 +9,5 @@ coverage/
.vscode/
.bundle
*gemfile.lock
.byebug_history
*.gem
37 changes: 31 additions & 6 deletions lib/jwt/decode.rb
Expand Up @@ -23,6 +23,7 @@ def decode_segments
validate_segment_count!
if @verify
decode_crypto
set_key
verify_signature
verify_claims
end
Expand All @@ -33,18 +34,32 @@ def decode_segments
private

def verify_signature
return unless @key || @verify

return if none_algorithm?

raise JWT::DecodeError, 'No verification key available' unless @key

return if Array(@key).any? { |key| verify_signature_for?(key) }

raise(JWT::VerificationError, 'Signature verification failed')
end

def set_key
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?

@key = find_key(&@keyfinder) if @keyfinder
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
end

Signature.verify(header['alg'], @key, signing_input, @signature)
def verify_signature_for?(key)
Signature.verify(algorithm, key, signing_input, @signature)
end

def options_includes_algo_in_header?
allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
end

def allowed_algorithms
Expand All @@ -65,8 +80,10 @@ def allowed_algorithms

def find_key(&keyfinder)
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
raise JWT::DecodeError, 'No verification key available' unless key
key
# key can be of type [string, nil, OpenSSL::PKey, Array]
return key if key && !Array(key).empty?

raise JWT::DecodeError, 'No verification key available'
end

def verify_claims
Expand All @@ -77,7 +94,7 @@ def verify_claims
def validate_segment_count!
return if segment_length == 3
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
return if segment_length == 2 && header['alg'] == 'none'
return if segment_length == 2 && none_algorithm?

raise(JWT::DecodeError, 'Not enough or too many segments')
end
Expand All @@ -86,10 +103,18 @@ def segment_length
@segments.count
end

def none_algorithm?
algorithm.casecmp('none').zero?
end

def decode_crypto
@signature = JWT::Base64.url_decode(@segments[2] || '')
end

def algorithm
header['alg']
end

def header
@header ||= parse_and_decode @segments[0]
end
Expand Down
7 changes: 1 addition & 6 deletions lib/jwt/signature.rb
Expand Up @@ -23,13 +23,8 @@ def sign(algorithm, msg, key)
end

def verify(algorithm, key, signing_input, signature)
return true if algorithm.casecmp('none').zero?

raise JWT::DecodeError, 'No verification key available' unless key

algo, code = Algos.find(algorithm)
verified = algo.verify(ToVerify.new(code, key, signing_input, signature))
raise(JWT::VerificationError, 'Signature verification raised') unless verified
algo.verify(ToVerify.new(code, key, signing_input, signature))
rescue OpenSSL::PKey::PKeyError
raise JWT::VerificationError, 'Signature verification raised'
ensure
Expand Down
1 change: 0 additions & 1 deletion spec/integration/readme_examples_spec.rb
Expand Up @@ -269,7 +269,6 @@
@cached_keys = nil if options[:invalidate] # need to reload the keys
@cached_keys ||= { keys: [jwk.export] }
end

expect do
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader})
end.not_to raise_error
Expand Down
112 changes: 112 additions & 0 deletions spec/jwt_spec.rb
Expand Up @@ -357,6 +357,118 @@
end

context 'Verify' do
context 'when key given as an array with multiple possible keys' do
let(:payload) { { 'data' => 'data'} }
let(:token) { JWT.encode(payload, secret, 'HS256') }
let(:secret) { 'hmac_secret' }

it 'should be able to verify signature when block returns multiple keys' do
decoded_token = JWT.decode(token, nil, true, { algorithm: 'HS256' }) do
['not_the_secret', secret]
end
expect(decoded_token.first).to eq(payload)
end

it 'should be able to verify signature when multiple keys given as a parameter' do
decoded_token = JWT.decode(token, ['not_the_secret', secret], true, { algorithm: 'HS256' })
expect(decoded_token.first).to eq(payload)
end

it 'should fail if only invalid keys are given' do
expect do
JWT.decode(token, ['not_the_secret', 'not_the_secret_2'], true, { algorithm: 'HS256' })
end.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end

context 'when encoded payload is used to extract key through find_key' do
it 'should be able to find a key using the block passed to decode' do
payload_data = { key: 'secret' }
token = JWT.encode payload_data, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { algorithm: 'HS256' }) do |_headers, payload|
data[payload['key'].to_sym]
end
end.not_to raise_error
end

it 'should be able to verify signature when block returns multiple keys' do
iss = 'My_Awesome_Company'
iss_payload = { data: 'data', iss: iss }

secrets = { iss => ['hmac_secret2', data[:secret]] }

token = JWT.encode iss_payload, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
secrets[payload['iss']]
end
end.not_to raise_error
end

it 'should be able to find a key using the block passed to decode with iss verification' do
iss = 'My_Awesome_Company'
iss_payload = { data: 'data', iss: iss }

secrets = { iss => data[:secret] }

token = JWT.encode iss_payload, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
secrets[payload['iss']]
end
end.not_to raise_error
end

it 'should be able to verify signature when block returns multiple keys with iss verification' do
iss = 'My_Awesome_Company'
iss_payload = { data: 'data', iss: iss }

secrets = { iss => ['hmac_secret2', data[:secret]] }

token = JWT.encode iss_payload, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { iss: iss, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
secrets[payload['iss']]
end
end.not_to raise_error
end

it 'should be able to find a key using a block with multiple issuers' do
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
iss_payload = { data: 'data', iss: issuers.first }

secrets = { issuers.first => data[:secret], issuers.last => 'hmac_secret2' }

token = JWT.encode iss_payload, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
secrets[payload['iss']]
end
end.not_to raise_error
end

it 'should be able to verify signature when block returns multiple keys with multiple issuers' do
issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
iss_payload = { data: 'data', iss: issuers.first }

secrets = { issuers.first => [data[:secret], 'hmac_secret1'], issuers.last => 'hmac_secret2' }

token = JWT.encode iss_payload, data[:secret], 'HS256'

expect do
JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
secrets[payload['iss']]
end
end.not_to raise_error
end
end

context 'algorithm' do
it 'should raise JWT::IncorrectAlgorithm on mismatch' do
token = JWT.encode payload, data[:secret], 'HS256'
Expand Down

0 comments on commit c4aa448

Please sign in to comment.