How-To:-Allow-users-to-sign-in-using-their-username-or-email-address.md
For this example, we will assume your model is called User.
users tablerails generate migration add_username_to_users username:string:uniq
rake db:migrate
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
added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :sign_in, keys: [:login, :password]
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
end
end
see also "strong parameters"
User modelAdd login as an User:
# app/models/user.rb
attr_writer :login
def login
@login || self.username || self.email
end
:login in the authentication_keysModify 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}
find_for_database_authentication method in User modelBecause 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).
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
elsif conditions.has_key?(:username) || conditions.has_key?(:email)
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 }
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
However, we need to be very careful with username validation, because there might be conflict between username and email. For example, given these users:
| id | username | |
|---|---|---|
| 1 | harrypotter | [email protected] |
| 2 | [email protected] | [email protected] |
The problem is at user #2, he is using #1's email as his username. What would happen if #1 try to log in by his email [email protected]? With the above query:
where(conditions).where(["username = :value OR lower(email) = lower(:value)", { :value => login }]).first
User #1 (harrypotter) will never be able to log in to the system, because that query always return #2.
To fix it, we can add one more validation to check format of username to not allow @ character:
# app/models/user.rb
# only allow letter, number, underscore and punctuation.
validates_format_of :username, with: /^[a-zA-Z0-9_\.]*$/, :multiline => true
You could also check if the same email as the username already exists in the database:
# app/models/user.rb
validate :validate_username
def validate_username
if User.where(email: username).exists?
errors.add(:username, :invalid)
end
end
However, note that relying ONLY on this second validation still allows a user to choose someone else's valid email address as a username, as long as that email is not yet associated with an account. This should therefore be used only as a backup validation.
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
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
sessions/new.html.erb:
- <p><%= f.label :email %>
- <%= f.email_field :email %></p>
+ <p><%= f.label :login %>
+ <%= f.text_field :login %></p>
Use :login and not :username in the sessions/new.html.erb
registrations/new.html.erb:
+ <p><%= f.label :username %>
+ <%= f.text_field :username %></p>
<p><%= f.label :email %>
<%= f.email_field :email %></p>
registrations/edit.html.erb:
+ <p><%= f.label :username %>
+ <%= f.text_field :username %></p>
<p><%= f.label :email %>
<%= f.email_field :email %></p>
Newer versions have different HTML.
This section assumes you have run through the steps in Allow users to Sign In using their username or email.
Simply modify config/initializers/devise.rb to have:
config.reset_password_keys = [ :username ]
config.confirmation_keys = [ :username ]
find_first_by_auth_conditions instead of find_for_database_authenticationReplace (in your User.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
passwords/new.html.erb:
- <p><%= f.label :email %>
- <%= f.email_field :email %></p>
+ <p><%= f.label :username %>
+ <%= f.text_field :username %></p>
confirmations/new.html.erb:
- <p><%= f.label :email %>
- <%= f.email_field :email %></p>
+ <p><%= f.label :username %>
+ <%= f.text_field :username %></p>
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 "[email protected]" 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 of using this find_for_database_authentication as this one causes problems.
# def self.find_for_database_authentication(conditions)
# where(:username => conditions[:email]).first || where(:email => conditions[:email]).first
# end