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

Handle parsed JSON JWKS input with string keys #348

Merged
merged 5 commits into from Sep 1, 2020
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
5 changes: 3 additions & 2 deletions lib/jwt/jwk.rb
Expand Up @@ -12,9 +12,10 @@ module JWK

class << self
def import(jwk_data)
raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_data[:kty]
jwk_kty = jwk_data[:kty] || jwk_data['kty']
raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty

MAPPINGS.fetch(jwk_data[:kty].to_s) do |kty|
MAPPINGS.fetch(jwk_kty.to_s) do |kty|
raise JWT::JWKError, "Key type #{kty} not supported"
end.import(jwk_data)
end
Expand Down
7 changes: 6 additions & 1 deletion lib/jwt/jwk/key_finder.rb
Expand Up @@ -14,6 +14,7 @@ def key_for(kid)

jwk = resolve_key(kid)

raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk

::JWT::JWK.import(jwk).keypair
Expand Down Expand Up @@ -45,8 +46,12 @@ def load_keys(opts = {})
@jwks = @jwk_loader.call(opts)
end

def jwks_keys
Array(jwks[:keys] || jwks['keys'])
end

def find_key(kid)
Array(jwks[:keys]).find { |key| key[:kid] == kid }
jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
end

def reloadable?
Expand Down
39 changes: 29 additions & 10 deletions lib/jwt/jwk/rsa.rb
Expand Up @@ -31,23 +31,42 @@ def kid
def export
{
kty: KTY,
n: ::Base64.urlsafe_encode64(public_key.n.to_s(BINARY), padding: false),
e: ::Base64.urlsafe_encode64(public_key.e.to_s(BINARY), padding: false),
n: encode_open_ssl_bn(public_key.n),
e: encode_open_ssl_bn(public_key.e),
kid: kid
}
end

def encode_open_ssl_bn(key_part)
::Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
end

def self.import(jwk_data)
imported_key = OpenSSL::PKey::RSA.new
if imported_key.respond_to?(:set_key)
imported_key.set_key(OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY),
OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY),
nil)
jwk_n = jwk_data[:n] || jwk_data['n']
jwk_e = jwk_data[:e] || jwk_data['e']

raise JWT::JWKError, 'Key format is invalid for RSA' unless jwk_n && jwk_e

self.new(rsa_pkey(jwk_n, jwk_e))
end

def self.rsa_pkey(jwk_n, jwk_e)
key = OpenSSL::PKey::RSA.new
key_n = decode_open_ssl_bn(jwk_n)
key_e = decode_open_ssl_bn(jwk_e)

if key.respond_to?(:set_key)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT::JWK::RSA#self.rsa_pkey manually dispatches method call

key.set_key(key_n, key_e, nil)
else
imported_key.n = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY)
imported_key.e = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY)
key.n = key_n
key.e = key_e
end
self.new(imported_key)

key
end

def self.decode_open_ssl_bn(jwk_data)
OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data), BINARY)
end
end
end
Expand Down
17 changes: 17 additions & 0 deletions spec/jwk/decode_with_jwk_spec.rb
Expand Up @@ -40,6 +40,15 @@
end
end

context 'no keys are found in the set' do
let(:public_jwks) { {keys: []} }
it 'raises an exception' do
expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks}) }.to raise_error(
JWT::DecodeError, /No keys found in jwks/
)
end
end

context 'token does not know the kid' do
let(:token_headers) { {} }
it 'raises an exception' do
Expand All @@ -64,5 +73,13 @@
expect(payload).to eq(token_payload)
end
end

context 'when jwk keys are loaded from JSON with string keys' do
it 'decodes the token' do
key_loader = ->(options) { JSON.parse(JSON.generate(public_jwks)) }
payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader})
expect(payload).to eq(token_payload)
end
end
end
end
30 changes: 30 additions & 0 deletions spec/jwk/rsa_spec.rb
Expand Up @@ -54,4 +54,34 @@
end
end
end

describe '.import' do
subject { described_class.import(params) }
let(:exported_key) { described_class.new(rsa_key).export }

context 'when keypair is imported with symbol keys' do
let(:params) { {e: exported_key[:e], n: exported_key[:n]} }
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a described_class
expect(subject.private?).to eq false
expect(subject.export).to eq(exported_key)
end
end

context 'when keypair is imported with string keys from JSON' do
let(:params) { {'e' => exported_key[:e], 'n' => exported_key[:n]} }
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a described_class
expect(subject.private?).to eq false
expect(subject.export).to eq(exported_key)
end
end

context 'when jwk_data is given without e and/or n' do
let(:params) { { kty: "RSA" } }
it 'raises an error' do
expect { subject }.to raise_error(JWT::JWKError, "Key format is invalid for RSA")
end
end
end
end
15 changes: 12 additions & 3 deletions spec/jwk_spec.rb
Expand Up @@ -8,13 +8,22 @@

describe '.import' do
let(:keypair) { rsa_key.public_key }
let(:params) { described_class.new(keypair).export }
let(:exported_key) { described_class.new(keypair).export }
let(:params) { exported_key }

subject { described_class.import(params) }

it 'creates a ::JWT::JWK::RSA instance' do
expect(subject).to be_a ::JWT::JWK::RSA
expect(subject.export).to eq(params)
expect(subject.export).to eq(exported_key)
end

context 'parsed from JSON' do
let(:params) { exported_key }
it 'creates a ::JWT::JWK::RSA instance from JSON parsed JWK' do
expect(subject).to be_a ::JWT::JWK::RSA
expect(subject.export).to eq(exported_key)
end
end

context 'when keytype is not supported' do
Expand All @@ -26,7 +35,7 @@
end
end

describe '.to_jwk' do
describe '.new' do
subject { described_class.new(keypair) }

context 'when RSA key is given' do
Expand Down