-
Notifications
You must be signed in to change notification settings - Fork 5.5k
How To: Allow users to sign in using their username or email address
For this example, we will assume your model is called User
.
rails 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
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"
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
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}
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).
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
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 %><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.
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.
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 ]
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
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>
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
# 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