Password Resets Using ActiveUrl
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!