Skip to content

Commit fc8aaf3

Browse files
committed
Add reset_password_verifies_account feature
This moves the related code from the reset_password feature to its own feature, so that users only have to pay the cost for the feature if they are using it. Update the spec to provide 100% coverage.
1 parent aa7b398 commit fc8aaf3

File tree

10 files changed

+77
-46
lines changed

10 files changed

+77
-46
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
=== master
2+
3+
* Add reset_password_verifies_account feature (manfrin, jeremyevans) (#499)
4+
15
=== 2.42.0 (2025-12-18)
26

37
* Avoid mixed string/symbol keys in JSON when using the jwt_refresh feature (jeremyevans)

README.rdoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ view the appropriate file in the doc directory.
6464
* {Remember}[rdoc-ref:doc/remember.rdoc]
6565
* {Reset Password}[rdoc-ref:doc/reset_password.rdoc]
6666
* {Reset Password Notify}[rdoc-ref:doc/reset_password_notify.rdoc]
67+
* {Reset Password Verifies Account}[rdoc-ref:doc/reset_password_verifies_account.rdoc]
6768
* {Session Expiration}[rdoc-ref:doc/session_expiration.rdoc]
6869
* {Single Session}[rdoc-ref:doc/single_session.rdoc]
6970
* {SMS Codes}[rdoc-ref:doc/sms_codes.rdoc]

demo-site/rodauth_demo.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class App < Roda
4949
plugin :rodauth, :json=>true, :csrf=>:route_csrf do
5050
db DB
5151
enable :change_login, :change_password, :close_account, :create_account,
52-
:lockout, :login, :logout, :remember, :reset_password, :verify_account,
52+
:lockout, :login, :logout, :remember, :reset_password_verifies_account,
5353
:otp_modify_email, :otp_lockout_email, :recovery_codes, :sms_codes, :disallow_common_passwords,
5454
:disallow_password_reuse, :password_grace_period, :active_sessions, :jwt,
5555
:verify_login_change, :change_password_notify, :confirm_password,

doc/reset_password.rdoc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ reset_password_email_subject :: The subject to use for the reset password reques
2323
reset_password_error_flash :: The flash error to show after resetting a password.
2424
reset_password_explanatory_text :: The text to display above the button to request a password reset.
2525
reset_password_id_column :: The id column in the +reset_password_table+, should be a foreign key referencing the accounts table.
26-
reset_password_implicitly_verifies? :: Whether resetting a password for an unverified account implicitly verifies the account, false by default.
2726
reset_password_key_column :: The reset password key/token column in the +reset_password_table+.
2827
reset_password_key_param :: The parameter name to use for the reset password key.
2928
reset_password_notice_flash :: The flash notice to show after resetting a password.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
= Documentation for Reset Password Feature
2+
3+
The reset password verifies account feature depends on both the reset
4+
password and verify account features, and makes it so that a valid
5+
password reset implicitly operates as an account verification, since
6+
it proves ownership of the related email address.

lib/rodauth/features/reset_password.rb

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ module Rodauth
3232
auth_value_method :reset_password_deadline_interval, {:days=>1}.freeze
3333
auth_value_method :reset_password_key_param, 'key'
3434
auth_value_method :reset_password_autologin?, false
35-
auth_value_method :reset_password_implicitly_verifies?, false
3635
auth_value_method :reset_password_table, :account_password_reset_keys
3736
auth_value_method :reset_password_id_column, :id
3837
auth_value_method :reset_password_key_column, :key
@@ -175,7 +174,7 @@ def create_reset_password_key
175174
end
176175

177176
def reset_password_request_for_unverified_account
178-
throw_error_reason(:unverified_account, unopen_account_error_status, login_param, unverified_account_message) unless reset_password_implicitly_verifies?
177+
throw_error_reason(:unverified_account, unopen_account_error_status, login_param, unverified_account_message)
179178
end
180179

181180
def remove_reset_password_key
@@ -232,14 +231,6 @@ def after_login_failure
232231
super
233232
end
234233

235-
def after_reset_password
236-
super if defined?(super)
237-
if reset_password_implicitly_verifies? && !open_account?
238-
verify_account
239-
remove_verify_account_key
240-
end
241-
end
242-
243234
def generate_reset_password_key_value
244235
@reset_password_key_value = random_key
245236
end
@@ -267,9 +258,7 @@ def _account_from_reset_password_key(token)
267258
end
268259

269260
def reset_password_account_status_value
270-
statuses = [account_open_status_value]
271-
statuses << account_unverified_status_value if reset_password_implicitly_verifies?
272-
statuses
261+
account_open_status_value
273262
end
274263
end
275264
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen-string-literal: true
2+
3+
module Rodauth
4+
Feature.define(:reset_password_verifies_account, :ResetPasswordVerifiesAccount) do
5+
depends :reset_password, :verify_account
6+
7+
def reset_password_request_for_unverified_account
8+
nil
9+
end
10+
11+
private
12+
13+
def after_reset_password
14+
super
15+
16+
unless open_account?
17+
verify_account
18+
remove_verify_account_key
19+
end
20+
end
21+
22+
def reset_password_account_status_value
23+
Array(super) << account_unverified_status_value
24+
end
25+
end
26+
end

spec/reset_password_spec.rb

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -226,37 +226,6 @@
226226
page.current_path.must_equal '/reset-password-request'
227227
end
228228

229-
it "should support implicit verification when resetting passwords for unverified accounts" do
230-
rodauth do
231-
enable :login, :reset_password, :verify_account
232-
reset_password_implicitly_verifies? true
233-
reset_password_autologin? true
234-
end
235-
roda do |r|
236-
r.rodauth
237-
r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
238-
end
239-
240-
DB[:accounts].update(:status_id=>1)
241-
DB[:account_verification_keys].insert(:id=>DB[:accounts].get(:id), :key=>'test')
242-
243-
visit '/reset-password-request'
244-
fill_in 'Login', :with=>'foo@example.com'
245-
click_button 'Request Password Reset'
246-
page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to reset the password for your account"
247-
248-
link = email_link(/(\/reset-password\?key=.+)$/)
249-
visit link
250-
fill_in 'Password', :with=>'0123456'
251-
fill_in 'Confirm Password', :with=>'0123456'
252-
click_button 'Reset Password'
253-
page.find('#notice_flash').text.must_equal "Your password has been reset"
254-
page.body.must_include("Logged In")
255-
256-
DB[:accounts].get(:status_id).must_equal 2
257-
DB[:account_verification_keys].count.must_equal 0
258-
end
259-
260229
[true, false].each do |before|
261230
it "should clear reset password token when closing account, when loading reset_password #{before ? "before" : "after"}" do
262231
rodauth do
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require_relative 'spec_helper'
2+
3+
describe 'Rodauth reset_password_verifies_account feature' do
4+
it "should support implicit verification when resetting passwords for unverified accounts" do
5+
rodauth do
6+
enable :login, :logout, :reset_password_verifies_account
7+
reset_password_autologin? true
8+
end
9+
roda do |r|
10+
r.rodauth
11+
r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
12+
end
13+
14+
DB[:accounts].update(:status_id=>1)
15+
DB[:account_verification_keys].insert(:id=>DB[:accounts].get(:id), :key=>'test')
16+
17+
2.times do |i|
18+
visit '/reset-password-request'
19+
fill_in 'Login', :with=>'foo@example.com'
20+
click_button 'Request Password Reset'
21+
page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to reset the password for your account"
22+
23+
link = email_link(/(\/reset-password\?key=.+)$/)
24+
visit link
25+
fill_in 'Password', :with=>"0123456#{i}"
26+
fill_in 'Confirm Password', :with=>"0123456#{i}"
27+
click_button 'Reset Password'
28+
page.find('#notice_flash').text.must_equal "Your password has been reset"
29+
page.body.must_include("Logged In")
30+
31+
DB[:accounts].get(:status_id).must_equal 2
32+
DB[:account_verification_keys].count.must_equal 0
33+
logout
34+
end
35+
end
36+
end

www/pages/documentation.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<li><a href="rdoc/files/doc/remember_rdoc.html">Remember</a>: Automatically logs a user in based on a token stored in a cookie.</li>
4949
<li><a href="rdoc/files/doc/reset_password_rdoc.html">Reset Password</a>: Allows users to reset their password if they don't remember it.</li>
5050
<li><a href="rdoc/files/doc/reset_password_notify_rdoc.html">Reset Password Notify</a>: Emails the user after they have used the Reset Password feature to successfully reset their password.</li>
51+
<li><a href="rdoc/files/doc/reset_password_verifies_account.html">Reset Password Verifies Account</a>: Allows password resets for unverified accounts, and verifies account on successful password reset.</li>
5152
<li><a href="rdoc/files/doc/session_expiration_rdoc.html">Session Expiration</a>: Expires sessions automatically based on inactivity or max lifetime checks.</li>
5253
<li><a href="rdoc/files/doc/single_session_rdoc.html">Single Session</a>: Allows only a single active session per account.</li>
5354
<li><a href="rdoc/files/doc/sms_codes_rdoc.html">SMS Codes</a>: Adds support for multifactor authentication via codes received via SMS.</li>

0 commit comments

Comments
 (0)