Forgot Password? 13
I did it again…forgot my password. Now if everyone could offer an openid login like Highrise. This time it happened on myconfplan, while I was organizing my schedule for next weeks RailsConf. As I didn’t find a link to reset the password on myconfplan, I send an email to their support. Dr Nic replied promptly and said he didn’t implement this yet on this wedsite, but he could manually reset the password. Well, recently I implemented that feature for MySpyder.net (one of our forthcoming web applications). So I send him some code snippets. Not sure if Dr Nic will use them, but maybe some of our blog readers may be interested, so here we go.
They are several ways to implement a “Forgot Password? This time we choose to send out a “reset password” link that is valid for 24 hours. This link lets the user login, bypassing the standard login, and showing the change password screen.
First lets add a migration.
class ForgotPassword < ActiveRecord::Migration
def self.up
add_column :users, :reset_password_code, :string
add_column :users, :reset_password_code_until, :datetime
end
def self.down
remove_column :users, :reset_password_code
remove_column :users, :reset_password_code_until
end
end“Forgot password” form.
Then add a “forgot password” form, allowing the user to submit the email to which the “reset password” link will be emailed. When the form is submitted, the controller creates a ‘reset password code’ that is valid for one day, and sends an email to the user.
def forgot_password
user = User.find_by_email(params[:email])
if (user)
user.reset_password_code_until = 1.day.from_now
user.reset_password_code = Digest::SHA1.hexdigest( "#{user.email}#{Time.now.to_s.split(//).sort_by {rand}.join}" )
user.save!
UserNotifier.deliver_forgot_password(user)
render :xml => "<errors><info>Reset Password link emailed to #{user.email}.</info></errors>"
else
render :xml => "<errors><error>User not found: #{params[:email]}</error></errors>"
end
endSend email with the ‘reset password’ link.
When the user receives the “reset password” email and clicks the link to reset the password, the reset_password method is invoked on the controller. The user associated with the “reset_code” is found, and if the the reset_code is not yet expired the user is automatically logged-in and redirected to the account page where he can change his password. Note that by adding an expiration attribute for the code, we don’t need to run a cleanup batch process to invalidate these codes. Not in the following code we redirect to a ”.swf” file. This was an early experiment where the user interface of the application was written in Flex. We are currently rewriting it to use a more traditional html and css approach.
def reset_password
user = User.find_by_reset_password_code(params[:reset_code])
self.current_user = user if user && user.reset_password_code_until && Time.now < user.reset_password_code_until
redirect_to logged_in? ? "/MySpyder.swf?a=account" : "/MySpyder.swf?a=login"
endThe email is simply send using the following ActionMailer.
class UserNotifier < ActionMailer::Base
def forgot_password(user)
setup_email(user)
@subject += 'MySpyder.net - Reset Password'
@body[:url] = "http://myspyder.net/reset_password/#{user.reset_password_code}"
end
protected
def setup_email(user)
@recipients = "#{user.email}"
@from = "admin@myspyder.net"
@subject = "[myspyder.net] "
@sent_on = Time.now
@body[:user] = user
end
endAnd the view for the UserNotifier is the following
<%= @user.email %>,
You can reset your password by using the following link <%= @url %>
Thank your for using MySpyder.net
I’d like to second Jim, thank you. It was a great help.
The only thing I would change would be a moderation to reset_password. If the reset password link has expired you will need to redirect the user appropriately and explain why. I think the best way is to redirec to the login page with a message.
Thanks Jim, the route is effectively an important part. And I like Paul’s suggestion as “link expired” page would add a nice touch.
The reset code is not very random, you should try to make this non-predictable :-) For example, I use:
reset_code = File.read(”/dev/urandom”, 8).unpack(“H*”)[0]
Thanks for the post. Good primer.
I assume the “forgot password” form should be called something other than forgot_password.rhtml ?? Thanks
@Surfraz Ahmed
it could be name forgot_password.rhtml!
i did the following: Controller: def forgot_password user = User.find_by_email(params[:email]) if (user) user.reset_password_code_until = 1.day.from_now user.reset_password_code = Digest::SHA1.hexdigest( ”#{user.email}#{Time.now.to_s.split(//).sort_by {rand}.join}” ) user.save! UserNotifier.deliver_forgot_password(user) notice_stickie(“Reset password link mailed to ”.t + user.email) redirect_to :controller => “firestation”, :action => “index” else # show form now here instead of errormessage end end
and made the form: <%= “Please insert your Email-Address”.t %><% form_tag :action => ‘forgot_password’ do %> <%= link_to_help “Help”.t, “user.email” %>
<%= “EMail”.t %>
<%= submit_tag “Request”.t %> <% end %><%= link_to icon_back, :action => ‘login’ %>
so it was pretty easy to do this with the same methodname ;) spared me the “ugly” xml ;)
UserNotifier.deliver_forgot_password(user) should not sit in the controller. Put it into an ActiveRecord::Observer
...The only thing I would change would be a moderation to reset_password. If the reset password link has expired you will need to redirect the user appropriately and explain why. I think the best way is to redirec to the login page with a message….
Regarding nicolash’s comment, what’s the argument for moving the deliver_forgot_password out of the controller?
I can see an argument for something like a “welcome” message, that’s connected to the User model on create event (although I do worry about unintentionally firing off e-mails from scripts and such).
But in this case, the forgot e-mail is inextricably tied to the forgot password request. Why introduce a level of indirection with an observer for that? What does an observer make simpler or less error-prone in this case?
I use:
reset_code = File.read(”/dev/urandom”, 8).unpack(“H*”)[0]
I assume the “forgot password” form should be called something other than forgot_password.rhtml ?? Thanks
I’m trying to istall a gadget on my desktop and it is requiring that i enter my user name and password. I forgot both my user name and password. How do i reteave it?
This article is very helpful. I always forget my passwords and now I don’t need to worry as much because of this.
Thanks!