User Registration, with Email Confirmation, in Ruby on Rails

User registration is function that is more or less common to all web applications. If your app has users, there has to be a way for them to register, right? Sometimes you want to add the extra step of confirming the user’s email address, to ensure that they have entered a valid address which they have access to. Here’s a way to handle user registration in Ruby on Rails that’s secure, robust, and handles email validation.

We’re going to start from scratch, so the first thing to do is create our application. Navigate to the directory where you want your app to live, and type

rails appname

where “appname” is the name you want to give your application.

Ok, now that we have our application set up, let’s create the models and controllers that we know we’ll be using. The only model we need for this example is a User model, and we’ll keep things simple and use a single controller called “Main.”

script/generate model User
script/generate controller Main

Now we’ve got our model and controller set up. We’re going to use database migrations for this tutorial; if you’re not used to using them, don’t worry, we’ll outline each step that you need to follow. To get started, open the file “/db/migrate/001_create_users.rb”. This file was created for us when we ran “script/generate model User”, and, for our purposes, it’s going to contain the database information about our User model.

What kind of database fields will we need for our User model? Obviously, we’ll need a username and password field, as well as an email address. Let’s also add a boolen “email_confirmed,” so that we can keep track of which users have confirmed their email address, and which haven’t. Also, instead of a “password” field, let’s use “hashed_password”, since we’re going to hash the password before it gets stored in the database (we’ll also put a limit of 40 on the field because that will be the size of our hash). Edit your 001_create_users.rb so that it looks like this:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column "username", :string
      t.column "hashed_password", :string, :limit => 40
      t.column "email", :string
      t.column "email_confirmed", :boolean, :default => false
    end
  end
 
  def self.down
    drop_table :users
  end
end
 

Now that our database migration is set up, we can go ahead and use it to generate our database schema. First, edit “/config/database.yml” so that it points to the database you’re using. Once you have the proper values entered there, go to the root of your app and type

rake db:migrate

Once the rake finishes, open up your database, and note that our “users” table was created, along with all the fields that we wanted. Notice also that the rake task created an “id” field, even though we didn’t define it ourselves in the migration file.

Now that our database is set up, let’s create our actions. We’ll need three actions, an “index” action that users will see when they go to the root section of our site, a register action, and an action that confirms the email address. Open up “/app/controllers/main_controller.rb” and add the following definitions

class MainController < ApplicationController
 
  # the root index action for our application
  def index
 
  end
 
  # allows guests to create a new User
  def register
 
  end
 
  # confirm an email address
  def confirm_email
 
  end
 
end
 

Since we have actions now, let’s set up our routes and our views. To configure the routes, open up “/app/config/routes.rb” and add the following routes:

  map.connect '', :controller => "main", :action => "index"
  map.connect 'register', :controller => "main", :action => "register"
  map.connect 'confirm_email/:hash', :controller => "main", :action => "confirm_email"

Now that our routes are hooked up, let’s create our views. Create the file “/app/views/main/index.rhtml”

<h1>Welcome!</h1>
<p>
Click <%= link_to "here", :controller => "main", :action => "register" %> to register.
</p>

and “/app/views/main/register.rhtml”

<%= error_messages_for 'user' %>
<h1>Register</h1>
<%= form_tag %>
<p>username<br/>
<%= text_field "user", "username", "maxlength" => 20, "size" => 20 %></p>
<p>email address<br/>
<%= text_field "user", "email", "maxlength" => 50, "size" => 20 %></p>
<p>password<br/>
<%= password_field "user", "password", "maxlength" => 20, "size" => 20 %></p>
<p>confirm password<br/>
<%= password_field "user", "confirm_password", "maxlength" => 20, "size" => 20 %></p>
<input type="submit" value="sign up" /></p>
<%= end_form_tag %>
 

We’re going to try to get away with not creating a view for our “confirm_email” action, so let’s leave alone that for now.

Alright, we’ve got our views set up, so let’s start writing some of the application code. Let’s start with the “register” action of our “main” controller. Basically, our registration function needs to grab the user parameters from the form in our “register.rhtml” view, create the user, and redirect to the login form. Simple enough. Here’s the code

  # allows guests to create a new User
  def register
 
    if request.get?
      @user = User.new
    else
      @user = User.new(params[:user])

      if @user.save
        flash[:notice] = "Thank you for registering! We have sent a confirmation email to #{@user.email} with instructions on how to validate your account."
        redirect_to(:action => "index")
      end
    end
 
  end

Now, you’ll notice two things that are very wrong. First of all, we’re telling the user that we sent him a confirmation email, but clearly we did no such thing. Also, we have no validation in our user model! Nothing is checking to make sure that the email address entered is properly formatted, that the username isn’t taken, that the password and confirm_password fields match, etc. Let’s open up our user model and add this validation now.

class User < ActiveRecord::Base
  validates_presence_of :username, :email
  validates_uniqueness_of :username
  validates_length_of :username, :minimum => 4
 
  attr_accessor :password, :confirm_password
 
  # callback hooks

    # validate that password and confirm_password match, and that email is proper format
    def validate_on_create
      @email_format = Regexp.new(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/)
      errors.add(:email, "must be a valid format") unless @email_format.match(email)
      errors.add(:confirm_password, "does not match") unless password == confirm_password
      errors.add(:password, "cannot be blank") unless !password or password.length > 0
    end
 
end
 

You’ll notice that we also added the line “attr_accessor :password, :confirm_password”. This is because we did not create a password or confirm_password field in our database, but we still want to access these properties on our user object. For this, we use the ruby method attr_accessor, which in this case essentially creates model properties for us which we can use like any other, but which are not stored in the database anywhere.

Now, we still need to figure out how to hash the password before it gets stored in the database. We could put this in our controller, but it really belongs in our user model. Rails provides us with lots of handy callback hooks, so let’s use one. We want to hash the password before the user is created, so we’ll use the hook “before_create”. We’ll also want to create a private function that does the actual hashing for us, and we’ll have to add a line at the top of our class to include the ruby class that does the hashing.

require "digest/sha1"
 
class User < ActiveRecord::Base
  validates_presence_of :username, :email
  validates_uniqueness_of :username
  validates_length_of :username, :minimum => 4

  attr_accessor :password, :confirm_password
 
  # callback hooks

    # validate that password and confirm_password match, and that email is proper format
    def validate_on_create
      @email_format = Regexp.new(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/)
      errors.add(:email, "must be a valid format") unless @email_format.match(email)
      errors.add(:confirm_password, "does not match") unless password == confirm_password
      errors.add(:password, "cannot be blank") unless !password or password.length > 0
    end
 
    # hash password before create
    def before_create
      self.hashed_password = User.hash_password(self.password)
    end

 
  private
 
  # hash password for storage in database
  def self.hash_password(password)
    Digest::SHA1.hexdigest(password)
  end

end
 

Now, we could stop here, but let’s make our user model a little more robust and secure. Let’s remove cleartext passwords from memory after the user is created, and let’s scrub email addresses and usernames to remove spaces and convert to lowercase. To do these two things, we’ll add two more callback hooks and another private function.

require "digest/sha1"
 
class User < ActiveRecord::Base
  validates_presence_of :username, :email
  validates_uniqueness_of :username
  validates_length_of :username, :minimum => 4

  attr_accessor :password, :confirm_password
 
  # callback hooks
 
    # clean up email and username before validation
    def before_validation
      self.email = User.clean_string(self.email || "")
      self.username = User.clean_string(self.username || "")
    end

    # validate that password and confirm_password match, and that email is proper format
    def validate_on_create
      @email_format = Regexp.new(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/)
      errors.add(:email, "must be a valid format") unless @email_format.match(email)
      errors.add(:confirm_password, "does not match") unless password == confirm_password
      errors.add(:password, "cannot be blank") unless !password or password.length > 0
    end
 
    # hash password before create
    def before_create
      self.hashed_password = User.hash_password(self.password)
    end

    # after creation, clear password from memory
    def after_create
      @password = nil
      @confirm_password = nil
    end

 
  private
 
  # hash password for storage in database
  def self.hash_password(password)
    Digest::SHA1.hexdigest(password)
  end
 
  # clean string to remove spaces and force lowercase
  def self.clean_string(string)
    (string.downcase).gsub(" ","")
  end

end
 

Alright, our user model is all done. It takes care of validation, hashes the password, cleans up email addresses and usernames, and minimizes the exposure of cleartext passwords. We’ve done a lot of work, so stand up, stretch, grab a beer, and come back.

Now, it’s time to add the mailer object that will send out the confirmation email. Before we do that, however, let’s take a moment to step back and think about how we’re going to handle email confirmation from an abstract level.

For email registration, we need to make sure that whatever method we use, the user cannot cheat and manually enter the URL to validate his email (no typing http://website.com/confirm_email/username). To prevent this, we need to use some kind of randomly-generated string. Now, we could just randomly generate a string and stick it in the database in our “users” table, but that’s sort of inelegant. Instead, it would be best if we could create some kind of seemingly random string, which the user cannot reverse engineer, but which we can recreate given known values. This is exactly what a hash is meant to be used for!

Let’s say, for example, that a user registers with the username “zaphod”. Converted to a hash using our algorithm, “zaphod” becomes “79118ddf1f07f13c3ddaf659f835f1ce4e1161f3″. We could then use that hash as a confirmation code, generating a confirmation url of “http://website.com/confirm_email/79118ddf1f07f13c3ddaf659f835f1ce4e1161f3″. When our app handles this URL, it just has to go through our list of users, look for one whose username, put through the hash function, matches “79118ddf1f07f13c3ddaf659f835f1ce4e1161f3″, and it will know which user is attempting to confirm! Note that the hash function is one-way, so we cannot just “unhash” the hash and match it up to a username.

Since we want to be secure, let’s go a step further. Instead of just hashing the username, let’s hash their username plus a secret word which we define. This prevents users from figuring out how we generate our hash (since they don’t know the inputs), and generating the hash themselves.

In our “main” controller, add the line to include the Ruby hash class, and add our private function for generating email confirmation hashes:

require "digest/sha1"
 
class MainController < ApplicationController
 
  # the root index action for our application
  def index
 
  end
 
  # allows guests to create a new User
  def register

    if request.get?
      @user = User.new
    else
      @user = User.new(params[:user])

      if @user.save
        flash[:notice] = "Thank you for registering! We have sent a confirmation email to #{@user.email} with instructions on how to validate your account."
        redirect_to(:action => "index")
      end
    end
 
  end
 
  # confirm an email address
  def confirm_email
 
  end
 
 
  private

  # create a hash to use when confirming User email addresses
  def confirmation_hash(string)
    Digest::SHA1.hexdigest(string + "secret word")
  end
 
end
 

Great, so now we know how we’re going to handle email confirmation, and we’ve got our function all set up to generate our hashes. Let’s create our mail robot that’s going to send out our emails. In the console, type

script/generate mailer MailRobot

This generates a model called MailRobot, which is going to handle all of our email stuff. The way ActionMailer works is that, for each email we want our app to be able to send, we need to create an action within our mailer model and a view to go along with it. Open up “/app/models/mail_robot.rb” and let’s add that action now.

class MailRobot < ActionMailer::Base
 
  def confirmation_email(user,hash)
    # email header info MUST be added here
    @recipients = user.email
    @from = "fromaddress@website-url-here.com"
    @subject = "Confirm email address"

    # email body substitutions go here
    @body["username"] = user.username
    @body["hash"] = hash
  end

end
 

Pretty simple. We’re just passing our user object and our confirmation hash to the action, setting up the “to,” “from,” and “subject” of the email, and defining some variables that we’re going to use in our view. Let’s create that view now; we’ll need to create “/app/views/mail_robot/confirmation_email.rhtml”

<%= @username %>,
 
Thank you for registering with our website. To confirm your email address, please visit the following address:
 
http://www.website-url-here.com/confirm_email/<%= @hash %>
 
- Friendly Mail Robot
 

One last thing we need to set up to get our mailer working. We need to edit our “/app/config/environment.rb” and add our mail settings to the end of the file. If you’re using DreamHost, your settings will look like this:

ActionMailer::Base.delivery_method = :sendmail
ActionMailer::Base.server_settings = {
  :address  => "mail.website-url-here.com",
  :port  => 25,
  :domain  => "website-url-here.com",
  :user_name  => "mXXXXXXX",
  :password  => "password",
  :authentication  => :login
    }
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.default_charset = "utf-8"
 

If you’re not using DreamHost, see this tutorial for help on getting these settings right.

Ok! Our mailer is all set up, so we can now go back to our “register” action in our “main” controller and add the code that will actually send out an email to our new user.

  # allows guests to create a new User
  def register
 
    if request.get?
      @user = User.new
    else
      @user = User.new(params[:user])

      if @user.save
        MailRobot::deliver_confirmation_email(@user, confirmation_hash(@user.username))

        flash[:notice] = "Thank you for registering! We have sent a confirmation email to #{@user.email} with instructions on how to validate your account."
        redirect_to(:action => "index")
      end
    end
 
  end

Now when a new user registers, they will get a confirmation email containing the confirmation hash we generated, formed as a url to our “confirm_email” action within our main controller. All that’s left to do is write that action.

What do we need this action to do? Basically, it just needs to go through all the users in our database, and for each user, generate a confirmation hash using that user’s username. Then, it needs to compare that hash to the hash that it’s receiving. If it finds a match, it should mark that user’s email address as confirmed, log that user in, and redirect to our index. Here’s the code:

  # confirm an email address
  def confirm_email
 
    @users = User.find :all

    for user in @users
      # if the passed hash matches up with a User, confirm him, log him in, set proper flash[:notice], and stop looking
      if confirmation_hash(user.username) == params[:hash] and !user.email_confirmed
        user.update_attributes(:email_confirmed => true)
        session[:user_id] = user.id
        flash[:notice] = "Thank you for validating your email."
        break
      end
    end
 
    redirect_to(:action => "index")
 
  end

That’s it! You’re all done. You now have a robust and secure login system for your app that validates users’ email addresses.

del.icio.us:User Registration, with Email Confirmation, in Ruby on Rails digg:User Registration, with Email Confirmation, in Ruby on Rails simpy:User Registration, with Email Confirmation, in Ruby on Rails newsvine:User Registration, with Email Confirmation, in Ruby on Rails reddit:User Registration, with Email Confirmation, in Ruby on Rails Y!:User Registration, with Email Confirmation, in Ruby on Rails

Comments

tim said,

January 17, 2007 at 9:51 am

Great post. I’m a rails newbie, but I love everything I’ve read so far. I was just curious, for the custom validation you wrote for your user model, is there a reason you chose to manually implement the validations for confirmation, format and length, instead of using some of the other validation helpers, like validates_confirmation_of, validates_format_of, or validates_length_of? I like the programatic approach too, I was just wondering if you were familiar with those awesome helpers. Thanks again for taking the time to write stuff like this!

Joey said,

January 17, 2007 at 10:08 am

Thanks for the reply, Tim.

The reason I wrote some of the validations manually is because I’m new to Rails myself, and so I’m still a bit stuck in my old ways and not used to all the handy features that the framework has to offer. You’re right, using the built-in ActiveRecord validations would be cleaner. Perhaps that should be the topic of another post!

Here’s the portion of the Rails API that explains all the different validations, by the way: http://www.rubyonrails.org/api/classes/ActiveRecord/Validations/ClassMethods.html