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

[POC] Use WebAuthn::RelyingParty #398

Conversation

santiagorodriguez96
Copy link
Contributor

@santiagorodriguez96 santiagorodriguez96 commented Mar 8, 2024

Motivation/Background

Rodauth currently overrides internal WebAuthn methods in order to be able to allow for multiple origins. This feature is officially supported by webauthn gem starting in its version v3 so we should be able to avoid overriding WebAuthn internal methods.

Details

This PR just changes to use the new webauthn instance based configuration to support for multiple origins. It feels to me that that should be all you need to do, but I might be missing something.

It also bumps webauthn required version to v3, but it shouldn't be hard to maintain compatibility with v2 if desired as v3 is backwards compatible.

Furthermore, nothing changes that much from v2 besides the required ruby version going from 2.3 in webauthn v2.0.0 to 2.5 in webauthn v3.0.0.

That way we can avoid overriding WebAuthn internal methods.
@@ -55,7 +55,7 @@ END
s.add_development_dependency('rotp')
s.add_development_dependency('rqrcode')
s.add_development_dependency('jwt')
s.add_development_dependency('webauthn', '>=2')
s.add_development_dependency('webauthn', '>=3')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want to still support v2 it shouldn’t be hard to maintain compatibility with v2 if desired as v3 is backwards compatible. We would have to do something like this:

diff --git a/lib/rodauth/features/webauthn.rb b/lib/rodauth/features/webauthn.rb
index 9af3cd3..b29e0fb 100644
--- a/lib/rodauth/features/webauthn.rb
+++ b/lib/rodauth/features/webauthn.rb
@@ -300,30 +300,35 @@ module Rodauth
     end

     def new_webauthn_credential
-      relying_party.options_for_registration(
+      WebAuthn::Credential.options_for_create(**{
         :timeout => webauthn_setup_timeout,
         :user => {:id=>account_webauthn_user_id, :name=>webauthn_user_name},
         :authenticator_selection => webauthn_authenticator_selection,
         :attestation => webauthn_attestation,
         :extensions => webauthn_extensions,
         :exclude => account_webauthn_ids,
-      )
+      }.merge(
+        WebAuthn::VERSION >= ‘3’ ? { :relying_party => relying_party } : { :rp => {:name=>webauthn_rp_name, :id=>webauthn_rp_id} }
+      ))
     end

-    def valid_new_webauthn_credential?(webauthn_setup_data)
+    def valid_new_webauthn_credential?(webauthn_credential)
+      _override_webauthn_credential_response_verify(webauthn_credential) unless WebAuthn::VERSION >= ‘3’
       (challenge = param_or_nil(webauthn_setup_challenge_param)) &&
         (hmac = param_or_nil(webauthn_setup_challenge_hmac_param)) &&
         (timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
-        relying_party.verify_registration(webauthn_setup_data, challenge)
+        webauthn_credential.verify(challenge)
     end

     def webauthn_credential_options_for_get
-      relying_party.options_for_authentication(
+      WebAuthn::Credential.options_for_get(**{
         :allow => webauthn_allow,
         :timeout => webauthn_auth_timeout,
         :user_verification => webauthn_user_verification,
         :extensions => webauthn_extensions,
-      )
+      }.merge(
+        WebAuthn::VERSION >= ‘3’ ? { :relying_party => relying_party } : { :rp_id => webauthn_rp_id }
+      ))
     end

     def webauthn_user_name
@@ -360,6 +365,7 @@ module Rodauth
       ds = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column => webauthn_credential.id)
       pub_key, sign_count = ds.get([webauthn_keys_public_key_column, webauthn_keys_sign_count_column])

+      _override_webauthn_credential_response_verify(webauthn_credential) unless WebAuthn::VERSION >= ‘3’
       (challenge = param_or_nil(webauthn_auth_challenge_param)) &&
         (hmac = param_or_nil(webauthn_auth_challenge_hmac_param)) &&
         (timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
@@ -414,6 +420,16 @@ module Rodauth
         )
     end

+    def _override_webauthn_credential_response_verify(webauthn_credential)
+      # Hack around inability to override expected_origin and rp_id
+      origin = webauthn_origin
+      rp_id = webauthn_rp_id
+      webauthn_credential.response.define_singleton_method(:verify) do |expected_challenge, expected_origin = nil, **kw|
+        kw[:rp_id] = rp_id
+        super(expected_challenge, expected_origin || origin, **kw)
+      end
+    end
+
     def _two_factor_auth_links
       links = super
       links << [10, webauthn_auth_path, webauthn_auth_link_text] if webauthn_setup? && !two_factor_login_type_match?(‘webauthn’)
@@ -459,7 +475,12 @@ module Rodauth

     def webauthn_auth_credential_from_form_submission
       begin
-        webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data, relying_party: relying_party)
+        webauthn_credential =
+          if WebAuthn::VERSION >= ‘3’
+            WebAuthn::Credential.from_get(webauthn_auth_data, relying_party: relying_party)
+          else
+            WebAuthn::Credential.from_get(webauthn_auth_data)
+          end
         unless valid_webauthn_credential_auth?(webauthn_credential)
           throw_error_reason(:invalid_webauthn_auth_param, invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
         end
@@ -493,7 +514,13 @@ module Rodauth
       end

       begin
-        unless webauthn_credential = valid_new_webauthn_credential?(webauthn_setup_data)
+        webauthn_credential =
+          if WebAuthn::VERSION >= ‘3’
+            WebAuthn::Credential.from_create(webauthn_setup_data, relying_party: relying_party)
+          else
+            WebAuthn::Credential.from_create(webauthn_setup_data)
+          end
+        unless valid_new_webauthn_credential?(webauthn_credential)
           throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
         end
       rescue WebAuthn::Error, RuntimeError, NoMethodError

However, nothing changes that much from v2 besides the required ruby version going from 2.3 in webauthn v2.0.0 to 2.5 in webauthn v3.0.0. so it feels a little bit unnecessary for me.

@@ -464,7 +459,7 @@ def webauthn_keys_ds

def webauthn_auth_credential_from_form_submission
begin
webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data)
webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data, relying_party: relying_party)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to go with this approach because using WebAuthn:RelyingParty seemed a little bit ugly, but I’ll leave it here just in case:

    diff --git a/lib/rodauth/features/webauthn.rb b/lib/rodauth/features/webauthn.rb
index 9af3cd3..52e6eda 100644
--- a/lib/rodauth/features/webauthn.rb
+++ b/lib/rodauth/features/webauthn.rb
@@ -356,18 +356,18 @@ module Rodauth
       nil
     end

-    def valid_webauthn_credential_auth?(webauthn_credential)
-      ds = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column => webauthn_credential.id)
+    def valid_webauthn_credential_auth?(webauthn_auth_data)
+      ds = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column => webauthn_auth_data[“id”])
       pub_key, sign_count = ds.get([webauthn_keys_public_key_column, webauthn_keys_sign_count_column])

       (challenge = param_or_nil(webauthn_auth_challenge_param)) &&
         (hmac = param_or_nil(webauthn_auth_challenge_hmac_param)) &&
         (timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
-        webauthn_credential.verify(challenge, public_key: pub_key, sign_count: sign_count) &&
+        (webauthn_credential = relying_party.verify_authentication(webauthn_auth_data, challenge, public_key: pub_key, sign_count: sign_count)) &&
         ds.update(
           webauthn_keys_sign_count_column => Integer(webauthn_credential.sign_count),
           webauthn_keys_last_use_column => Sequel::CURRENT_TIMESTAMP
-        ) == 1
+        ) == 1 && webauthn_credential
     end

     def remove_webauthn_key(webauthn_id)
@@ -459,8 +459,7 @@ module Rodauth

     def webauthn_auth_credential_from_form_submission
       begin
-        webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data, relying_party: relying_party)
-        unless valid_webauthn_credential_auth?(webauthn_credential)
+        unless webauthn_credential = valid_webauthn_credential_auth?(webauthn_auth_data)
           throw_error_reason(:invalid_webauthn_auth_param, invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
         end
       rescue WebAuthn::SignCountVerificationError

@@ -498,8 +493,7 @@ def webauthn_setup_credential_from_form_submission
end

begin
webauthn_credential = WebAuthn::Credential.from_create(webauthn_setup_data)
unless valid_new_webauthn_credential?(webauthn_credential)
unless webauthn_credential = valid_new_webauthn_credential?(webauthn_setup_data)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps you prefer the old-like approach of passing the relying_party?

        webauthn_credential = WebAuthn::Credential.from_create(webauthn_setup_data, relying_party: relying_party)
        unless valid_new_webauthn_credential?(webauthn_credential)

@jeremyevans
Copy link
Owner

Awesome, thanks for your work on this! I would like to keep webauthn v2 support, at least until Rodauth 3.

@jeremyevans
Copy link
Owner

I've merged this with the webauthn v2 compatibility changes, and then did a little refactoring: 99a986f

@santiagorodriguez96 santiagorodriguez96 deleted the sr--use-webauthn-relying-party branch March 15, 2024 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants