Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support to be able to verify from multiple keys #425

Merged
merged 1 commit into from Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
ritikesh marked this conversation as resolved.
Show resolved Hide resolved
ritikesh marked this conversation as resolved.
Show resolved Hide resolved
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'
ritikesh marked this conversation as resolved.
Show resolved Hide resolved
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