Skip to content

Commit

Permalink
Support hmac_secret rotation in webauthn feature
Browse files Browse the repository at this point in the history
This only checks that the submitted challenges match either the
current or previous hmac.  The only time you would need to support
long hmac rotation period for webauthn is if you wanted to allow
users to go to the webauthn setup/auth page and then actually
submit the form on that page much later.
  • Loading branch information
jeremyevans committed Sep 29, 2023
1 parent dee3b8d commit a474d2d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Support hmac_secret rotation in webauthn feature (jeremyevans) (#365)

* Support hmac_secret rotation in jwt_refresh feature (jeremyevans) (#365)

* Support hmac_secret rotation in single_session feature (jeremyevans) (#365)
Expand Down
4 changes: 2 additions & 2 deletions lib/rodauth/features/webauthn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def valid_new_webauthn_credential?(webauthn_credential)

(challenge = param_or_nil(webauthn_setup_challenge_param)) &&
(hmac = param_or_nil(webauthn_setup_challenge_hmac_param)) &&
timing_safe_eql?(compute_hmac(challenge), hmac) &&
(timing_safe_eql?(compute_hmac(challenge), hmac) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(challenge), hmac))) &&
webauthn_credential.verify(challenge)
end

Expand Down Expand Up @@ -376,7 +376,7 @@ def valid_webauthn_credential_auth?(webauthn_credential)

(challenge = param_or_nil(webauthn_auth_challenge_param)) &&
(hmac = param_or_nil(webauthn_auth_challenge_hmac_param)) &&
timing_safe_eql?(compute_hmac(challenge), hmac) &&
(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) &&
ds.update(
webauthn_keys_sign_count_column => Integer(webauthn_credential.sign_count),
Expand Down
49 changes: 49 additions & 0 deletions spec/webauthn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

it "should handle webauthn authentication" do
hmac_secret = '123'
hmac_old_secret = nil
before_setup = nil
before_remove = nil
rodauth do
enable :login, :logout, :webauthn
hmac_secret do
hmac_secret
end
hmac_old_secret do
hmac_old_secret
end
before_webauthn_setup do
before_setup.call if before_setup
end
Expand Down Expand Up @@ -90,6 +94,29 @@
hmac_secret = '123'
visit page.current_path

challenge = JSON.parse(page.find('#webauthn-setup-form')['data-credential-options'])['challenge']
fill_in 'Password', :with=>'0123456789'
fill_in 'webauthn_setup', :with=>webauthn_client.create(challenge: challenge).to_json
hmac_secret = '321'
hmac_old_secret = '333'
click_button 'Setup WebAuthn Authentication'
page.find('#error_flash').text.must_equal 'Error setting up WebAuthn authentication'
hmac_secret = '123'
visit page.current_path
setup_path = page.current_path

hmac_secret = '321'
hmac_old_secret = '123'
challenge = JSON.parse(page.find('#webauthn-setup-form')['data-credential-options'])['challenge']
fill_in 'Password', :with=>'0123456789'
fill_in 'webauthn_setup', :with=>webauthn_client.create(challenge: challenge).to_json
click_button 'Setup WebAuthn Authentication'
page.find('#notice_flash').text.must_equal 'WebAuthn authentication is now setup'
DB[:account_webauthn_keys].delete

hmac_secret = '123'
hmac_old_secret = nil
visit setup_path
challenge = JSON.parse(page.find('#webauthn-setup-form')['data-credential-options'])['challenge']
fill_in 'Password', :with=>'0123456789'
webauthn_hash = webauthn_client.create(challenge: challenge)
Expand Down Expand Up @@ -140,6 +167,28 @@
hmac_secret = '123'
visit page.current_path

challenge = JSON.parse(page.find('#webauthn-auth-form')['data-credential-options'])['challenge']
fill_in 'webauthn_auth', :with=>webauthn_client.get(challenge: challenge).to_json
hmac_secret = '321'
hmac_old_secret = '333'
click_button 'Authenticate Using WebAuthn'
page.find('#error_flash').text.must_equal 'Error authenticating using WebAuthn'
hmac_secret = '123'
visit page.current_path
auth_path = page.current_path

hmac_secret = '321'
hmac_old_secret = '123'
challenge = JSON.parse(page.find('#webauthn-auth-form')['data-credential-options'])['challenge']
fill_in 'webauthn_auth', :with=>valid_webauthn_client.get(challenge: challenge).to_json
click_button 'Authenticate Using WebAuthn'
page.find('#notice_flash').text.must_equal 'You have been multifactor authenticated'

hmac_secret = '123'
hmac_old_secret = nil
logout
login
visit auth_path
challenge = JSON.parse(page.find('#webauthn-auth-form')['data-credential-options'])['challenge']
fill_in 'webauthn_auth', :with=>webauthn_client.get(challenge: challenge).to_json
sign_count = DB[:account_webauthn_keys].get(:sign_count)
Expand Down

0 comments on commit a474d2d

Please sign in to comment.