diff --git a/lib/webauthn/authenticator_assertion_response.rb b/lib/webauthn/authenticator_assertion_response.rb index 1511117c..6ae46482 100644 --- a/lib/webauthn/authenticator_assertion_response.rb +++ b/lib/webauthn/authenticator_assertion_response.rb @@ -37,8 +37,22 @@ def initialize(authenticator_data:, signature:, user_handle: nil, **options) @user_handle = user_handle end - def verify(expected_challenge, expected_origin = nil, public_key:, sign_count:, user_verification: nil, rp_id: nil) - super(expected_challenge, expected_origin, user_verification: user_verification, rp_id: rp_id) + def verify( + expected_challenge, + expected_origin = nil, + public_key:, + sign_count:, + user_presence: nil, + user_verification: nil, + rp_id: nil + ) + super( + expected_challenge, + expected_origin, + user_presence: user_presence, + user_verification: user_verification, + rp_id: rp_id + ) verify_item(:signature, WebAuthn::PublicKey.deserialize(public_key)) verify_item(:sign_count, sign_count) diff --git a/lib/webauthn/public_key_credential_with_assertion.rb b/lib/webauthn/public_key_credential_with_assertion.rb index e82e5378..71d70d51 100644 --- a/lib/webauthn/public_key_credential_with_assertion.rb +++ b/lib/webauthn/public_key_credential_with_assertion.rb @@ -9,13 +9,14 @@ def self.response_class WebAuthn::AuthenticatorAssertionResponse end - def verify(challenge, public_key:, sign_count:, user_verification: nil) + def verify(challenge, public_key:, sign_count:, user_presence: nil, user_verification: nil) super response.verify( encoder.decode(challenge), public_key: encoder.decode(public_key), sign_count: sign_count, + user_presence: user_presence, user_verification: user_verification, rp_id: appid_extension_output ? appid : nil ) diff --git a/lib/webauthn/relying_party.rb b/lib/webauthn/relying_party.rb index b1b556bc..6a667c48 100644 --- a/lib/webauthn/relying_party.rb +++ b/lib/webauthn/relying_party.rb @@ -99,6 +99,7 @@ def options_for_authentication(**keyword_arguments) def verify_authentication( raw_credential, challenge, + user_presence: nil, user_verification: nil, public_key: nil, sign_count: nil @@ -111,6 +112,7 @@ def verify_authentication( challenge, public_key: public_key || stored_credential.public_key, sign_count: sign_count || stored_credential.sign_count, + user_presence: user_presence, user_verification: user_verification ) block_given? ? [webauthn_credential, stored_credential] : webauthn_credential diff --git a/spec/webauthn/authenticator_assertion_response_spec.rb b/spec/webauthn/authenticator_assertion_response_spec.rb index 5aac9fd5..b0d3f95d 100644 --- a/spec/webauthn/authenticator_assertion_response_spec.rb +++ b/spec/webauthn/authenticator_assertion_response_spec.rb @@ -103,19 +103,217 @@ end describe "user present validation" do - let(:assertion) { client.get(challenge: original_challenge, user_present: false, user_verified: false) } + context "when user presence flag is off" do + let(:assertion) { client.get(challenge: original_challenge, user_present: false, user_verified: false) } + + context "when silent_authentication is not set" do + context 'when user presence is not set' do + it "doesn't verify" do + expect { + assertion_response.verify(original_challenge, public_key: credential_public_key, sign_count: 0) + }.to raise_exception(WebAuthn::UserPresenceVerificationError) + end + + it "is invalid" do + expect( + assertion_response.valid?(original_challenge, public_key: credential_public_key, sign_count: 0) + ).to be_falsy + end + end - context "if user flags are off" do - it "doesn't verify" do - expect { - assertion_response.verify(original_challenge, public_key: credential_public_key, sign_count: 0) - }.to raise_exception(WebAuthn::UserPresenceVerificationError) + context 'when user presence is not required' do + it "verifies if user presence is not required" do + expect( + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + + it "is valid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + end + + context 'when user presence is required' do + it "doesn't verify" do + expect { + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + }.to raise_exception(WebAuthn::UserPresenceVerificationError) + end + + it "is invalid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + ).to be_falsy + end + end end - it "is invalid" do - expect( - assertion_response.valid?(original_challenge, public_key: credential_public_key, sign_count: 0) - ).to be_falsy + context "when silent_authentication is disabled" do + around do |ex| + old_value = WebAuthn.configuration.silent_authentication + WebAuthn.configuration.silent_authentication = false + + ex.run + + WebAuthn.configuration.silent_authentication = old_value + end + + context 'when user presence is not set' do + it "doesn't verify" do + expect { + assertion_response.verify(original_challenge, public_key: credential_public_key, sign_count: 0) + }.to raise_exception(WebAuthn::UserPresenceVerificationError) + end + + it "is invalid" do + expect( + assertion_response.valid?(original_challenge, public_key: credential_public_key, sign_count: 0) + ).to be_falsy + end + end + + context 'when user presence is not required' do + it "verifies if user presence is not required" do + expect( + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + + it "is valid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + end + + context 'when user presence is required' do + it "doesn't verify" do + expect { + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + }.to raise_exception(WebAuthn::UserPresenceVerificationError) + end + + it "is invalid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + ).to be_falsy + end + end + end + + context "when silent_authentication is enabled" do + around do |ex| + old_value = WebAuthn.configuration.silent_authentication + WebAuthn.configuration.silent_authentication = true + + ex.run + + WebAuthn.configuration.silent_authentication = old_value + end + + context 'when user presence is not set' do + it "verifies if user presence is not required" do + expect( + assertion_response.verify(original_challenge, public_key: credential_public_key, sign_count: 0) + ).to be_truthy + end + + it "is valid" do + expect( + assertion_response.valid?(original_challenge, public_key: credential_public_key, sign_count: 0) + ).to be_truthy + end + end + + context 'when user presence is not required' do + it "verifies if user presence is not required" do + expect( + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + + it "is valid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: false + ) + ).to be_truthy + end + end + + context 'when user presence is required' do + it "doesn't verify" do + expect { + assertion_response.verify( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + }.to raise_exception(WebAuthn::UserPresenceVerificationError) + end + + it "is invalid" do + expect( + assertion_response.valid?( + original_challenge, + public_key: credential_public_key, + sign_count: 0, + user_presence: true + ) + ).to be_falsy + end + end end end end diff --git a/spec/webauthn/public_key_credential_with_assertion_spec.rb b/spec/webauthn/public_key_credential_with_assertion_spec.rb index 574d5b40..34b147c9 100644 --- a/spec/webauthn/public_key_credential_with_assertion_spec.rb +++ b/spec/webauthn/public_key_credential_with_assertion_spec.rb @@ -352,5 +352,41 @@ end end end + + context "when user_presence" do + context "is not set" do + it "correcly delegates its value to the response" do + expect(assertion_response).to receive(:verify).with(anything, hash_including(user_presence: nil)) + + public_key_credential.verify(challenge, public_key: credential_public_key, sign_count: credential_sign_count) + end + end + + context "is set to false" do + it "correcly delegates its value to the response" do + expect(assertion_response).to receive(:verify).with(anything, hash_including(user_presence: false)) + + public_key_credential.verify( + challenge, + public_key: credential_public_key, + sign_count: credential_sign_count, + user_presence: false + ) + end + end + + context "is set to true" do + it "correcly delegates its value to the response" do + expect(assertion_response).to receive(:verify).with(anything, hash_including(user_presence: true)) + + public_key_credential.verify( + challenge, + public_key: credential_public_key, + sign_count: credential_sign_count, + user_presence: true + ) + end + end + end end end diff --git a/spec/webauthn/relying_party_spec.rb b/spec/webauthn/relying_party_spec.rb index a8a5bc9f..ea9488f7 100644 --- a/spec/webauthn/relying_party_spec.rb +++ b/spec/webauthn/relying_party_spec.rb @@ -68,6 +68,71 @@ end end + describe '#verify_authentication' do + let(:options) { admin_rp.options_for_authentication(allow: user.credentials.map(&:webauthn_id)) } + let(:raw_credential) { admin_fake_client.get(challenge: options.challenge, rp_id: admin_rp.id, sign_count: 1) } + + let(:admin_credential) { create_credential(client: admin_fake_client, relying_party: admin_rp) } + let(:admin_credential_public_key) { admin_credential[1] } + + before do + user.credentials << OpenStruct.new( + webauthn_id: admin_credential.first, + public_key: admin_rp.encoder.encode(admin_credential[1]), + sign_count: 0 + ) + end + + context "when user_presence" do + let(:webauthn_credential_mock) { instance_double('WebAuthn::PublicKeyCredentialWithAssertion', verify: true) } + + before do + allow(WebAuthn::Credential).to receive(:from_get).and_return(webauthn_credential_mock) + end + + context "is not set" do + it "correcly delegates its value to the response" do + expect(webauthn_credential_mock).to receive(:verify).with(anything, hash_including(user_presence: nil)) + + admin_rp.verify_authentication( + raw_credential, + options.challenge, + public_key: admin_credential_public_key, + sign_count: 0 + ) + end + end + + context "is set to false" do + it "correcly delegates its value to the response" do + expect(webauthn_credential_mock).to receive(:verify).with(anything, hash_including(user_presence: false)) + + admin_rp.verify_authentication( + raw_credential, + options.challenge, + public_key: admin_credential_public_key, + sign_count: 0, + user_presence: false + ) + end + end + + context "is set to true" do + it "correcly delegates its value to the response" do + expect(webauthn_credential_mock).to receive(:verify).with(anything, hash_including(user_presence: true)) + + admin_rp.verify_authentication( + raw_credential, + options.challenge, + public_key: admin_credential_public_key, + sign_count: 0, + user_presence: true + ) + end + end + end + end + context "without having any global configuration" do let(:consumer_rp) do WebAuthn::RelyingParty.new(