-
Notifications
You must be signed in to change notification settings - Fork 97
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
[POC] Use WebAuthn::RelyingParty
#398
Conversation
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') |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
Awesome, thanks for your work on this! I would like to keep webauthn v2 support, at least until Rodauth 3. |
I've merged this with the webauthn v2 compatibility changes, and then did a little refactoring: 99a986f |
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 versionv3
so we should be able to avoid overridingWebAuthn
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 tov3
, but it shouldn't be hard to maintain compatibility withv2
if desired asv3
is backwards compatible.Furthermore, nothing changes that much from
v2
besides the required ruby version going from2.3
inwebauthn v2.0.0
to2.5
inwebauthn v3.0.0
.