From dc47bc0cc7e909b9fc00986c70e5aab097387da9 Mon Sep 17 00:00:00 2001 From: Ritikesh Date: Mon, 13 Dec 2021 14:26:52 +0530 Subject: [PATCH] Add Support to be able to verify from multiple keys --- .gitignore | 2 + lib/jwt/decode.rb | 37 ++++++-- lib/jwt/signature.rb | 7 +- spec/integration/readme_examples_spec.rb | 1 - spec/jwt_spec.rb | 112 +++++++++++++++++++++++ 5 files changed, 146 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 9a787ab8..04ad34a0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ coverage/ .vscode/ .bundle *gemfile.lock +.byebug_history +*.gem diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 4677056e..e23e3773 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -23,6 +23,7 @@ def decode_segments validate_segment_count! if @verify decode_crypto + set_key verify_signature verify_claims end @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/jwt/signature.rb b/lib/jwt/signature.rb index e490d9eb..5dcb3489 100644 --- a/lib/jwt/signature.rb +++ b/lib/jwt/signature.rb @@ -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 diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index a384a44d..fe352ffc 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -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 diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index b9978a7f..98f0ae7c 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -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'