Password Resets Using ActiveUrl

Posted 22 May 2009

In my previous article I described my ActiveUrl gem for Ruby on Rails applications, using a new-user registration page as an example. Let’s take a look at another application of the library – implementing a “reset password” function. Basically, we want to allow an user to change his/her password without logging in. We’ll achieve this by sending the secret URL to the user when they submit a “forgot your password?” form.

Again, the basic idea is to hide the password-editing page behind the secret URL. The password-editing page will not be protected by the usual authentication requirements; instead, the knowledge of the secret URL is what authenticates the user.

Model

Let’s first take a look at an ActiveUrl model for the secret URL. We want to create an instance from an email address, which is what the user will still know once the password is forgotten. We could declare an email attribute as in the previous article, but the only thing our model really needs is a reference to a user, which we can derive from the email.

For this purpose, we’ll use the belongs_to feature of ActiveUrl. This is a quick-and-dirty mirror of the corresponding ActiveRecord feature. (Its only purpose though is to relate a secret URL to an existing database record, so it’s only got the bare minimum of functionality.) Let’s use it:

class Secret < ActiveUrl::Base
  belongs_to :user
  validates_presence_of :user
  
  attr_reader :email
  attr_accessible :email
  
  def email=(email)
    @email = email
    self.user = User.find_by_email(email)
  end

  after_save :send_email
  
  protected
  
  def send_email
    Mailer.deliver_secret(self)
  end
end

Attributes

We’ve set the email as a virtual attribute, just as we might for a normal ActiveRecord object. In addition to setting an instance variable, the email setter method also sets the user. The Secret#user= method is generated by the belongs_to association. (user_id=, user and user_id methods are also generated.)

We can see what attributes are stored in the model, and what can be written by mass-assignment:

Secret.attribute_names
# => #<Set: {:user_id}>

Secret.accessible_attributes
# => #<Set: {:email}>

In other words, the only attribute stored in the model is the user id, but that id can only be set by setting the email.

User.first
# => #<User id: 1, email: "name@example.com", ... >

secret = Secret.new(:user_id => 1)
secret.user_id
# => nil

secret = Secret.new(:email => "name@example.com")
secret.user_id
# => 1

Validations

A validation, validates_presence_of :user, ensures that an existing user is found for the given email address. The object won’t save (and the email won’t get sent) if there’s no user with that email address.

(n.b. If you want to use the Rails error markup in your form, you might want to set an error on email instead.)

Callbacks

Finally, note the after_save callback. It’s a method which sends the secret URL to the user in an email, and it will get called when the controller successfully saves the object.

Routes

Our routes are pretty simple. We only want to be able to create secrets, so we’ll just have new and create routes. Nested under a secret, we want some routes for changing the user’s password. This could be arranged in a few different ways, but let’s put the password-changing actions in their own controller:

map.resources :secrets, :only => [ :new, :create ] do |secret|
  secret.resources :passwords, :only => [ :new, :create ]
end

Controller

As always, we strive for generic controllers, and we pretty much get one here:

class SecretsController < ApplicationController
  def new
    @secret = Secret.new
  end
  
  def create
    @secret = Secret.new(params[:secret])
    if @secret.save
      flash[:notice] = "Please check your email for a link to change your password."
      redirect_to root_path # or wherever...
    else
      flash.now[:error] = "Unrecognised email address" # if you want to disclose this...
      render :action => "new"
    end
  end
end

Of course, there’s also a PasswordController, which will contain the actions for changing the user’s password. (The user to edit will be obtained from the secret, which in turn will be found from params[:secret_id].) Its implementation will depend on the User model. Since these actions are hidden behind the secret URL, we’d want to skip the normal user authentication filters for the actions.

View

How does the user actually request a password reset? By submitting his/her email address in a form. Link to this form on the login page:

<%= link_to "I forgot my password", new_secret_path %>

The form itself just asks for an email address:

<% form_for @secret do |form| %>
  <p>
    OK, so you forgot your password.
    No problems! Just enter your email address.
    We'll send you a link to change your password.
  </p>
  <div>
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>
  <div>
    <%= form.submit "OK" %>
  </div>
<% end %>

Mailer

In our mailer we want to send an email containing the secret URL for the password edit action. The ActiveUrl object obtained from the URL contains all we need to know, so we just pass it through to the email template. We send the email to the secret’s associated user:

class Mailer < ActionMailer::Base
  def secret(secret)
    subject    "Change password requested"
    recipients secret.user.email
    from       "admin@website.com"
    
    body       :secret => secret
  end
end

The email template might look something like:

Hi <%= @secret.user.first_name %>,

To change your password, please visit the following link:

<%= new_secret_password_url(@secret, :host => "website.com") %>

(If you did not request a password change, just ignore this email.)

Thanks!
website.com

Expiring the URL

There’s a potential problem with the above implementation though. As it stands, the secret URL is static – the password reset URL for any given user will always be the same. This may or may not be a problem, depending on your security requirements.

It would be nice to have the URL expire once the password has been changed – in effect, to have a single-use URL. This is easily done. We add an attribute to the model containing the current password hash (or the cleartext password, if you store your user passwords in the clear – you shouldn’t):

attribute :password_hash

def email=(email)
  @email = email
  self.user = User.find_by_email(email)
  self.password_hash = user.password_hash if user
end

Then, simply validate the password hash to ensure it’s the same as the user’s:

validate :password_hash_is_current, :if => :user

def password_hash_is_current
  errors.add(:password_hash) unless user.password_hash == password_hash
end

Since ActiveUrl::Base.find only finds valid objects, once the password has been changed, the secret URL won’t validate and an ActiveUrl::RecordNotFound error will be raised. The controller will then drop through to a 404. Easy!

Feed-icon-14x14 Comments