Forgot Password? 13

Posted by Daniel Wanja Wed, 09 May 2007 14:31:02 GMT

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.

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.

UserController#forgot_password
  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 
  end

Send 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.

UserController#reset_password
  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"    
  end

The email is simply send using the following ActionMailer.

UserNotifier
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
end

And the view for the UserNotifier is the following

forgot_password.erb
<%= @user.email %>,

You can reset your password by using the following link <%= @url %>

Thank your for using MySpyder.net
Comments

Leave a response

  1. Paul Brackenridge Fri, 18 May 2007 10:10:32 GMT

    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.

  2. Daniel Wanja Fri, 18 May 2007 14:27:50 GMT

    Thanks Jim, the route is effectively an important part. And I like Paul’s suggestion as “link expired” page would add a nice touch.

  3. HD Sun, 10 Jun 2007 19:15:21 GMT

    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]

  4. Steve Fri, 20 Jul 2007 17:01:48 GMT

    Thanks for the post. Good primer.

  5. Surfraz Ahmed Wed, 08 Aug 2007 14:38:11 GMT

    I assume the “forgot password” form should be called something other than forgot_password.rhtml ?? Thanks

  6. sjoker Sun, 12 Aug 2007 19:13:08 GMT

    @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 ;)

  7. nicolash Fri, 28 Sep 2007 07:45:57 GMT

    UserNotifier.deliver_forgot_password(user) should not sit in the controller. Put it into an ActiveRecord::Observer

  8. proxy site Mon, 15 Oct 2007 13:35:59 GMT

    ...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….

  9. mla Sun, 28 Oct 2007 21:55:03 GMT

    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?

  10. anonymous proxy Mon, 05 Nov 2007 13:02:08 GMT

    I use:

    reset_code = File.read(”/dev/urandom”, 8).unpack(“H*”)[0]

  11. automotive repair manual Sat, 19 Jan 2008 05:12:32 GMT

    I assume the “forgot password” form should be called something other than forgot_password.rhtml ?? Thanks

  12. Karen Freeman Sat, 15 Nov 2008 03:00:58 GMT

    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?

  13. math calculater Sat, 06 Dec 2008 23:31:01 GMT

    This article is very helpful. I always forget my passwords and now I don’t need to worry as much because of this.

    Thanks!

Comments