Skip to content

How To: Allow users to sign_in using their username or email address

obrok edited this page Dec 12, 2011 · 18 revisions

h2. Allow users to Sign In using their username or email address

For this example, we will assume your model is called Users

h3. Create a username field in Users

Create a migration:

   rails generate migration add_username_to_users username:string

Run the migration:

   rake db:migrate

Modify the User model and add username to attr_accessible

   attr_accessible :username

h3. Create a login virtual attribute in Users

Add login as an attr_accessor

  # Virtual attribute for authenticating by either username or email
  # This is in addition to a real persisted field like 'username'
  attr_accessor :login

Add login to attr_accessible

  attr_accessible :login

h3. Tell Devise to use :login in the authentication_keys

Modify config/initializers/devise.rb to have:

   config.authentication_keys = [ :login ]

Overwrite Devise's find_for_database_authentication method in Users

  • For ActiveRecord:
   def self.find_for_database_authentication(warden_conditions)
     conditions = warden_conditions.dup
     login = conditions.delete(:login)
     where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.strip.downcase }]).first
   end
  • For Mongoid: Note: This code for Mongoid does some small things differently then the ActiveRecord code above. Would be great if someone could port the complete functionality of the ActiveRecord code over to Mongoid [basically you need to port the 'where(conditions)']. It is not required but will allow greater flexibility.
  field :email

  def self.find_for_authentication(conditions)
    login = conditions.delete(:login)
    self.any_of({ :username => login }, { :email => login }).first
  end
  • For MongoMapper:
  def self.find_for_authentication(conditions)
    login = conditions.delete(:login).downcase
    where('$or' => [{:username => login}, {:email => login}]).first
  end

Older versions may have called find_for_authentication find_for_database_authentication.

h3. Update your views

Make sure you have the Devise views in your project so that you can customize them

Rails 3:

   rails g devise:views

Rails 2:

   script/generate devise_views

Modify the views

#* sessions/new.html.erb:

  -  <p><%= f.label :email %><br />
  -  <%= f.email_field :email %></p>
  +  <p><%= f.label :login %><br />
  +  <%= f.text_field :login %></p>

#* registrations/new.html.erb

  +  <p><%= f.label :username %><br />
  +  <%= f.text_field :username %></p>
     <p><%= f.label :email %><br />
     <%= f.email_field :email %></p>

#* registrations/edit.html.erb

  +  <p><%= f.label :username %><br />
  +  <%= f.text_field :username %></p>
     <p><%= f.label :email %><br />
     <%= f.email_field :email %></p>

h3. Manipulate the :login label that Rails will display

Modify config/locales/en.yml to contain something like:

Rails 2:

  activemodel:
    attributes:
      user:
        login: "Username or email"

Rails 3:

  en:
    activerecord:
      attributes:
        user:  
          login: "Username or email"

h2. Allow users to recover their password using either username or email address

This section assumes you have run through the steps in Allow users to Sign In using their username or password.

h3. Tell Devise to use :login in the reset_password_keys

Modify config/initializers/devise.rb to have:

   config.reset_password_keys = [ :login ]

h3. Overwrite Devise's find_for_database_authentication method in Users

  • For ActiveRecord:
   protected

   # Attempt to find a user by it's email. If a record is found, send new
   # password instructions to it. If not user is found, returns a new user
   # with an email not found error.
   def self.send_reset_password_instructions(attributes={})
     recoverable = find_recoverable_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
     recoverable.send_reset_password_instructions if recoverable.persisted?
     recoverable
   end 

   def self.find_recoverable_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
     (case_insensitive_keys || []).each { |k| attributes[k].try(:downcase!) }

     attributes = attributes.slice(*required_attributes)
     attributes.delete_if { |key, value| value.blank? }

     if attributes.size == required_attributes.size
       if attributes.has_key?(:login)
          login = attributes.delete(:login)
          record = find_record(login)
       else  
         record = where(attributes).first
       end  
     end  

     unless record
       record = new

       required_attributes.each do |key|
         value = attributes[key]
         record.send("#{key}=", value)
         record.errors.add(key, value.present? ? error : :blank)
       end  
     end  
     record
   end

   def self.find_record(login)
     where(["username = :value OR email = :value", { :value => login }]).first
   end
  • For Mongoid:
  def self.find_record(login)
    found = where(:username => login).to_a
    found = where(:email => login).to_a if found.empty?
    found
  end

For Mongoid this can be optimized using a "custom javascript function":http://omarqureshi.net/posts/2010/06/17/mongoid-or-query/

  def self.find_record(login)
    where("function() {return this.username == '#{login}' || this.email == '#{login}'}")
  end  
  • For MongoMapper:
  def self.find_record(login)
    (self.where(:email => login[:login]).first || self.where(:username => login[:login]).first) rescue nil
  end

h3. Update your views

Modify the views

#* passwords/new.html.erb:

  -  <p><%= f.label :email %><br />
  -  <%= f.email_field :email %></p>
  +  <p><%= f.label :login %><br />
  +  <%= f.text_field :login %></p>

h2. Gmail or me.com Style

Another way to do this is me.com and gmail style. You allow an email or the username of the email. For public facing accounts, this has more security. Rather than allow some hacker to enter a username and then just guess the password, they would have no clue what the user's email is. Just to make it easier on the user for logging in, allow a short form of their email to be used e.g "someone@domain.com" or just "someone" for short.

before_create :create_login

  def create_login             
    email = self.email.split(/@/)
    login_taken = User.where( :login => email[0]).first
    unless login_taken
      self.login = email[0]
    else	
      self.login = self.email
    end	       
  end

  def self.find_for_database_authentication(conditions)
    self.where(:login => conditions[:email]).first || self.where(:email => conditions[:email]).first
  end

For the Rails 2 version (1.0 tree): There is no @find_for_database_authentication@ method, so use @self.find_for_authentication@ as the finding method.

  def self.find_for_authentication(conditions)
    conditions = ["username = ? or email = ?", conditions[authentication_keys.first], conditions[authentication_keys.first]]
    super
  end
Clone this wiki locally