Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

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

lermit4 edited this page · 42 revisions
Clone this wiki locally

Allow users to Sign In using their username or email address

For this example, we will assume your model is called User.

Create a username field in the users table

Create a migration:

rails generate migration add_username_to_users username:string:uniq

Run the migration:

rake db:migrate

Strong parameters

Modify application_controller.rb and add username, email, password, password confirmation and remember me to configure_permitted_parameters

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation, :remember_me) }
    devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :username, :email, :password, :remember_me) }
    devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:username, :email, :password, :password_confirmation, :current_password) }
  end
end

see also "strong parameters"

Create a login virtual attribute in the User model

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

or, if you will use this variable somewhere else in the code:

  def login=(login)
    @login = login
  end

  def login
    @login || self.username || self.email
  end

Tell Devise to use :login in the authentication_keys

Modify config/initializers/devise.rb to have:

config.authentication_keys = [ :login ]

If you are using multiple models with Devise, it is best to set the authentication_keys on the model itself if the keys may differ:

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, 
         :validatable, :authentication_keys => [:login]

If you need permissions, you should implement that in a before filter. You can also supply a hash where the value is a boolean determining whether or not authentication should be aborted when the value is not present.

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, 
         :validatable, :authentication_keys => {email: true, login: false}

Overwrite Devise's find_for_database_authentication method in User model

Because we want to change the behavior of the login action, we have to overwrite the find_for_database_authentication method. The method's stack works like this: find_for_database_authentication calls find_for_authentication which calls find_first_by_auth_conditions. Overriding the find_for_database_authentication method allows you to edit database authentication; overriding find_for_authentication allows you to redefine authentication at a specific point (such as token, LDAP or database). Finally, if you override the find_first_by_auth_conditions method, you can customize finder methods (such as authentication, account unlocking or password recovery).

For ActiveRecord:

MySQL users: the use of the SQL lower function below is most likely unnecessary and will cause any index on the email column to be ignored.

# app/models/user.rb

    def self.find_for_database_authentication(warden_conditions)
      conditions = warden_conditions.dup
      if login = conditions.delete(:login)
        where(conditions.to_h).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
      else
        where(conditions.to_h).first
      end
    end

If Rails4: use to_hash instead; or the to_h would be empty hash because by default no permitted params are set in passwords_controller.rb.

And if you want email to be case insensitive, you should add

conditions[:email].downcase! if conditions[:email]
where(conditions.to_h).first

Be sure to add case insensitivity to your validations on :username:

# app/models/user.rb

validates :username,
  :presence => true,
  :uniqueness => {
    :case_sensitive => false
  } # etc.

Alternatively, change the find conditions like so:

# when allowing distinct User records with, e.g., "username" and "UserName"...
where(conditions).where(["username = :value OR lower(email) = lower(:value)", { :value => login }]).first

For Mongoid:

Note: This code for Mongoid does some small things differently than 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_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login)
      self.any_of({ :username =>  /^#{Regexp.escape(login)}$/i }, { :email =>  /^#{Regexp.escape(login)}$/i }).first
    else
      super
    end
  end

The code below also supports Mongoid but uses the where method and the OR operator to choose between username and email.

  # function to handle user's login via email or username
  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login).downcase
      where(conditions).where('$or' => [ {:username => /^#{Regexp.escape(login)}$/i}, {:email => /^#{Regexp.escape(login)}$/i} ]).first
    else
      where(conditions).first
    end
  end 

Update your views

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

rails g devise:views

or

rails g devise:views users

in case you have multiple models and you want customized views.

Simply modify config/initializers/devise.rb file to have

config.scoped_views = true

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>

Newer versions have different HTML.

Manipulate the :login label that Rails will display

In your config/locales/devise.en.yml, change

     invalid: "Invalid email or password."
     ...
     not_found_in_database: "Invalid email or password."

to

     invalid: "Invalid login or password."
     ...
     not_found_in_database: "Invalid login or password."

If instead you see

    invalid: "Invalid %{authentication_keys} or password."
    ...
    not_found_in_database: "Invalid %{authentication_keys} or password."

you don't have to make any changes.

Allow users to recover their password or confirm their account using their username

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

Configure Devise to use username as reset password or confirmation keys

Simply modify config/initializers/devise.rb to have:

config.reset_password_keys = [ :username ]
config.confirmation_keys = [ :username ]

Use find_first_by_auth_conditions instead of find_for_database_authentication

Replace (in your Users.rb):

def self.find_for_database_authentication(warden_conditions)
  conditions = warden_conditions.dup
  if login = conditions.delete(:login)
    where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
  else
    where(conditions).first
  end
end

with:

def self.find_first_by_auth_conditions(warden_conditions)
  conditions = warden_conditions.dup
  if login = conditions.delete(:login)
    where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
  else
    if conditions[:username].nil?
      where(conditions).first
    else
      where(username: conditions[:username]).first
    end
  end
end

Update your views

passwords/new.html.erb:

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

confirmations/new.html.erb:

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

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.

  after_initialize :create_login, :if => :new_record?

  def create_login
    if self.username.blank?
      email = self.email.split(/@/)
      login_taken = Pro.where(:username => email[0]).first
      unless login_taken
        self.username = email[0]
      else    
        self.username = self.email
      end   
    end     
  end

  # You might want to use the self.find_first_by_auth_conditions(warden_conditions) above instead
  # instead of using this find_for_database_authentication as this one causes problems.
  # def self.find_for_database_authentication(conditions)
  #   self.where(:yusername => conditions[:email]).first || self.where(:email => conditions[:email]).first
  # end
Something went wrong with that request. Please try again.