diff --git a/CHANGELOG b/CHANGELOG index a9ee01e9..04896126 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ === master +* Support hmac_secret rotation in email_base feature (jeremyevans) (#365) + * Support hmac_secret rotation in webauthn feature (jeremyevans) (#365) * Support hmac_secret rotation in jwt_refresh feature (jeremyevans) (#365) diff --git a/lib/rodauth/features/email_base.rb b/lib/rodauth/features/email_base.rb index 0133a781..24a6538e 100644 --- a/lib/rodauth/features/email_base.rb +++ b/lib/rodauth/features/email_base.rb @@ -69,12 +69,10 @@ def account_from_key(token, status_id=nil) return unless actual = yield(id) - unless timing_safe_eql?(key, convert_email_token_key(actual)) - if hmac_secret && allow_raw_email_token? - return unless timing_safe_eql?(key, actual) - else - return - end + unless (hmac_secret && timing_safe_eql?(key, convert_email_token_key(actual))) || + (hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual))) || + ((!hmac_secret || allow_raw_email_token?) && timing_safe_eql?(key, actual)) + return end ds = account_ds(id) ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks? diff --git a/spec/verify_account_spec.rb b/spec/verify_account_spec.rb index 0f110325..b7f86c87 100644 --- a/spec/verify_account_spec.rb +++ b/spec/verify_account_spec.rb @@ -3,13 +3,14 @@ describe 'Rodauth verify_account feature' do it "should support verifying accounts" do last_sent_column = nil - secret = nil + secret = old_secret = nil allow_raw_token = false rodauth do enable :login, :create_account, :verify_account verify_account_autologin? false verify_account_email_last_sent_column{last_sent_column} hmac_secret{secret} + hmac_old_secret{old_secret} allow_raw_email_token?{allow_raw_token} verify_account_set_password? false require_login_confirmation? true @@ -89,6 +90,11 @@ visit link page.find('#error_flash').text.must_equal "There was an error verifying your account: invalid verify account key" + secret = SecureRandom.random_bytes(32) + old_secret = SecureRandom.random_bytes(32) + visit link + page.find('#error_flash').text.must_equal "There was an error verifying your account: invalid verify account key" + allow_raw_token = true visit link click_button 'Verify Account' @@ -103,11 +109,13 @@ [false, true].each do |ph| it "should support setting passwords when verifying accounts #{'with account_password_hash_column' if ph}" do initial_secret = secret = SecureRandom.random_bytes(32) + old_secret = nil rodauth do enable :login, :create_account, :verify_account account_password_hash_column :ph if ph verify_account_autologin? false hmac_secret{secret} + hmac_old_secret{old_secret} end roda do |r| r.rodauth @@ -125,6 +133,15 @@ visit link page.find('#error_flash').text.must_equal "There was an error verifying your account: invalid verify account key" + secret = SecureRandom.random_bytes(32) + old_secret = SecureRandom.random_bytes(32) + visit link + page.find('#error_flash').text.must_equal "There was an error verifying your account: invalid verify account key" + + old_secret = initial_secret + visit link + page.find_by_id('password')[:autocomplete].must_equal 'new-password' + secret = initial_secret visit link page.find_by_id('password')[:autocomplete].must_equal 'new-password'