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.







tim said,
January 17, 2007 at 9:51 amGreat 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!