RoR Devise: Sign in with username OR email - ruby-on-rails

What's the best way to enable users to log in with their email address OR their username? I am using warden + devise for authentication. I think it probably won't be too hard to do it but i guess i need some advice here on where to put all the stuff that is needed. Perhaps devise already provides this feature? Like in the config/initializers/devise.rb you would write:
config.authentication_keys = [ :email, :username ]
To require both username AND email for signing in. But i really want to have only one field for both username and email and require only one of them. I'll just visualize that with some ASCII art, it should look something like this in the view:
Username or Email:
[____________________]
Password:
[____________________]
[Sign In]

I have found a solution for the problem. I'm not quite satisfied with it (I'd rather have a way to specify this in the initializer), but it works for now. In the user model I added the following method:
def self.find_for_database_authentication(conditions={})
find_by(username: conditions[:email]) || find_by(email: conditions[:email])
end
As #sguha and #Chetan have pointed out, another great resource is available on the official devise wiki.

From their Wiki — How To: Allow users to sign in using their username or email address.

def self.find_for_authentication(conditions)
conditions = ["username = ? or email = ?", conditions[authentication_keys.first], conditions[authentication_keys.first]]
# raise StandardError, conditions.inspect
super
end
Use their example!

Make sure you already added username field and add username to attr_accessible.
Create a login virtual attribute in Users
1) 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
2) Add login to attr_accessible
attr_accessible :login
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
# Overrides the devise method find_for_authentication
# Allow users to Sign In using their username or email address
def self.find_for_authentication(conditions)
login = conditions.delete(:login)
where(conditions).where(["username = :value OR email = :value", { :value => login }]).first
end
Update your views
Make sure you have the Devise views in your project so that you can customize them
remove <%= f.label :email %>
remove <%= f.email_field :email %>
add <%= f.label :login %>
add <%= f.text_field :login %>

https://gist.github.com/867932 : One solution for everything. Sign in, forgot password, confirmation, unlock instructions.

Platforma Tec (devise author) has posted a solution to their github wiki which uses an underlying Warden authentication strategy rather than plugging into the Controller:
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address
(An earlier answer had a broken link, which I believe was intended to link to this resource.)

If you are using MongoDB (with MongoId), you need to query differently:
def self.find_for_database_authentication(conditions={})
self.any_of({name: conditions[:email]},{email: conditions[:email]}).limit(1).first
end
just so it will be somewhere online.

With squeel gem you can do:
def self.find_for_authentication(conditions={})
self.where{(email == conditions[:email]) | (username == conditions[:email])}.first
end

I wrote like this and it works out. Don't know if it's "ugly fix", but if I'll come up with a a better solution I'll let you know...
def self.authenticate(email, password)
user = find_by_email(email) ||
username = find_by_username(email)
if user && user.password_hash = BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end

I use a quick hack for this, to avoid changing any devise specific code and use it for my specific scenario (I particularly use it for an API where mobile apps can create users on the server).
I have added a before_filter to all the devise controllers where if username is being passed, I generate an email from the username ("#{params[:user][:username]}#mycustomdomain.com") and save the user. For all other calls as well, I generate the email based on same logic. My before_filter looks like this:
def generate_email_for_username
return if(!params[:user][:email].blank? || params[:user][:username].blank?)
params[:user][:email] = "#{params[:user][:username]}#mycustomdomain.com"
end
I am also saving username in the users table, so I know that users with email ending in #mycustomdomain.com were created using username.

Here's a Rails solution which refactors #padde's answer. It uses ActiveRecord's find_by to simplify the calls, ensures there's only one call based on the regex, and also supports numeric IDs if you want to allow that (useful for scripts/APIs). The regex for email is as simple as it needs to be in this context; just checking for the presence of an # as I assume your username validtor doesn't allow # characters.
def self.find_for_database_authentication(conditions={})
email = conditions[:email]
if email =~ /#/
self.find_by_email(email)
elsif email.to_s =~ /\A[0-9]+\z/
self.find(Integer(email))
else
self.find_by_username(email])
end
end
Like the wiki and #aku's answer, I'd also recommend making a new :login parameter using attr_accessible and authentication_keys instead of using :email here. (I kept it as :email in the example to show the quick fix.)

Related

Rails devise username should be nil when for admin type user

I have follow the device documentation here to allow user sign in using their username or email address.
I want to allow user with admin: true only login with their email only. So from this question is it possible to do something like:
if current_user.admin?
config.authentication_keys = [ :username ]
else
config.authentication_keys = [ :login ] # can use username/email
end
or is it any better solution for this?
This will not work as the config is done in devise.rb, which is an initializer that runs only once (when the application is loaded).
I don't see an easy way to make this work - but what would be the problem to allows anyone to use either their email or username?

How to match a new string with the password already encrypted

I am working on a rails 3.2.13 project. I am using devise plugin (devise gem 3.2.2, 1.4.2) for authentication. Using this plugin, how can I validate the current_password field while changing the old password to a new one? Or else, please suggest how I can achieve this by encrypting the given string and matching it with the password already saved without using the devise plugin.
E.g.: One user has encrypted_password like below:
"$2a$10$VrawKYj6zp10XUxbixVzE.7d4QgYjQn9aiuzAuP7fp3PZOLMP5wbu"
while changing the password, if I enter a current_password, it should match the string above (encrypted_password == current_password). How can I validate this?
I believe you need to break your problem down into the following steps:
Determine if the old_password is actually the user's current password.
To do this, you can call:
User.find_by_id([SOME_ID]).valid_password?(old_password)
If this returns true, then you can move on to the next step to begin changing of the password. If it doesn't, then the old_password is incorrect, and you should not allow the changing of password.
The implementation of valid_password? can be found in the Devise gem's /lib/devise/models/database_authenticatable.rb file (at around Line 40). You could use this implementation to roll your own code for validating a password. But, Devise pretty much does it for you if you call valid_password?, so rolling your own seems unnecessary.
If old_password is valid, then verify that new_password matches confirm_new_password.
if (new_password == confirm_new_password)
.
.
.
end
If these match, then set the new password by doing the following:
u = User.find_by_id([SOME ID])
u.password = new_password
u.password_confirmation = confirm_new_password
u.save
You can verify that the password has been changed by:
u.valid_password?(new_password)
Update user with current_password validation:
#user.update_with_password(account_update_params)
# account_update_params - should have :current_password, :password, :password_confirmation
It is default behaviour in Devise::RegistrationsController. If you want update user without password, you should overwrite controller's action
class UsersController < Devise::RegistrationsController
def update_resource(resource, params)
# resource.update_with_password(params)
resource.update_attributes(params)
end
end
Do I understand you right what you want allow users login with encrypted and unencrypted (usual) password?
We have:
user.valid_password?('Password2').should
code on github
So we can overwrite it inside models/user.rb
def valid_password?(password)
encrypted_password == password || super(password)
end

Displaying User Password With Devise Confirmation Page

I'm attempting to display a users password along in his confirmation page sent by the Devise mailer. The confirmation page is the default
Welcome test0#test.com!
You can confirm your account email through the link below:
Confirm my account
However, I wish to have
Welcome test0#test.com!
Your password is currently DASADSADS
You can confirm your account email through the link below:
Confirm my account
How do I access the user object in the view? Do I need to override the mailer controller with a custom one? If so, how do I tell what the methods of the current mailer do (tried looking at documentation but can't find any clues)?
I noticed that #email and #resource are used in the view. Can I use any of these to access the current password in its unhashed form?
Note that I am sending this email manually with user.find(1).send_confirmation_instructions
Although this can be done, I would caution very strongly against doing so. Hashed passwords are specifically used so that the password cannot be recreated easily. Passing the original password back to the user will cause it to be sent back in plain text which sort of defeats the whole purpose. Also, shouldn't the user already know their password (they did type it in twice after all)?!?
To do this, you would need to capture the original (unhashed) password in the registration create action and send the email at that point (passing along the password). You can do this by overriding the sign_up method - you can do this in an initializer:
class Devise::RegistrationsController < DeviseController
def sign_up(resource_name, resource)
sign_in(resource_name, resource)
resource.unhashed_password = resource_params[:password]
resource.send_confirmation_instructions
end
end
Alternatively, you can derive a new controller from Devise::RegistrationsController and put this override code there (the recommended approach - but then again, this whole operation isn't really recommended). You'll need to add the unhashed_password accessor for this to work:
class User < ActiveRecord::Base
attr_accessor :unhashed_password
end
And then you can update your confirmation view (at app/views/devise/mailer/confirmation_instructions.html.erb) to contain this:
<p>Your password is currently <%= #resource.unhashed_password %></p>
Devise save password in encrypted form: You can decrypt it using,
Generate new migration:
$ rails g migration AddLegacyPasswordToUser legacy_password:boolean
invoke active_record
create db/migrate/20120508083355_add_legacy_password_to_users.rb
$ rake db:migrate
Using legacy_password method in following code you can decrypt your password:
class User < ActiveRecord::Base
...
def valid_password?(password)
if self.legacy_password?
# Use Devise's secure_compare to avoid timing attacks
if Devise.secure_compare(self.encrypted_password, User.legacy_password(password))
self.password = password
self.password_confirmation = password
self.legacy_password = false
self.save!
else
return false
end
end
super(password)
end
# Put your legacy password hashing method here
def self.legacy_password(password)
return Digest::MD5.hexdigest("#{password}-salty-herring");
end
end
You can just use request.request_parameters[:user][:password] to get the plain text password on the create or update action.

Rails Devise: Set password reset token and redirect user

In my app for a certain use case I create a new user (programmatically set the password) and send them a confirmation email.
I would like them to be able to change their password immediately after confirming (without having to enter the system generated one which I don't want to send them)
In effect I would like
1) System creates a new user account with generated password.
2) System sends confirmation email.
3) User clicks confirmation and is redirected to enter in their password (effectively send them to a URL like below)
Change my password
Any help / pointers would be great.
A simple way to have just one step for users to confirm email address and set initial password using the link you proposed...
Send one email your app generates, including a reset_password_token, and consider user's possession of that token confirmation of the validity of that email address.
In system account generation code, assuming User model is set up with :recoverable and :database_authenticatable Devise modules...
acct = User.new
acct.password = User.reset_password_token #won't actually be used...
acct.reset_password_token = User.reset_password_token
acct.email = "user#usercompany.com" #assuming users will identify themselves with this field
#set other acct fields you may need
acct.save
Make the devise reset password view a little clearer for users when setting initial password.
views/devise/passwords/edit.html.erb
...
<%= "true" == params[:initial] ? "Set your password" : "Reset your password" %>
...
Generated Email
Hi <%= #user.name %>
An account has been generated for you.
Please visit www.oursite.com/users/password/edit?initial=true&reset_password_token=<%= #user.reset_password_token %> to set your password.
No need to include :confirmable Devise module in your User model, since accounts created by your app won't get accessed without the reset_password_token in the email.
Devise will handle the submit and clear the reset_password_token field.
See devise_gem_folder/lib/devise/models/recoverable.rb and database_authenticatable.rb for details on reset_password_token method and friends.
If you want to use Devise :confirmable module rather than this approach, see the Devise wiki page.
In Rails 4.1, the following modification of Anatortoise House's reply works:
user = User.new
user.password = SecureRandom.hex #some random unguessable string
raw_token, hashed_token = Devise.token_generator.generate(User, :reset_password_token)
user.reset_password_token = hashed_token
user.reset_password_sent_at = Time.now.utc
user.email = 'user#usercompany.com'
user.save!
# Use a mailer you've written, such as:
AccountMailer.set_password_notice(user, raw_token).deliver
The email view has this link:
www.oursite.com/users/password/edit?initial=true&reset_password_token=<%= #raw_token %>
Here is my snippet for mailer preview
class Devise::MailerPreview < ActionMailer::Preview
def reset_password_instructions
user = User.last
token = user.send(:set_reset_password_token)
Devise::Mailer.reset_password_instructions(user, token)
end
end
You can call
user.send(:set_reset_password_token)
It may not be stable, as it's a protected method but it can work for your case. Just cover it with a test.
(tested on Devise v. 3.4)

How to auto-generate passwords in Rails Devise?

I am trying out how Devise works with one of my projects for user authentication. There is a user requirement that their admin should be able to generate a batch of username and user's password from time to time, and then the admin will email the new username and password to his users.
Assume the admin has the knowledge of direct SQL on the MySQL database, how can the generated usernames/passwords recognized by Devise? Thanks!
Use the Devise.friendly_token method:
password_length = 6
password = Devise.friendly_token.first(password_length)
User.create!(:email => 'someone#something.com', :password => password, :password_confirmation => password)
FYI: Devise.friendly_token returns a 20 character token. In the example above, we're chopping off the first password_length characters of the generated token by using the String#first method that Rails provides.
One option would be to use the Devise.generate_token. I.e.
password = User.generate_token('password')
User.create!(:email => 'someone#something.com', :password => password, :password_confirmation => password)
This option has not been available in Devise for quite a while. Please refer to the other answer (friendly_token).
I'm using devise-security gem and have specefic password_complexity requirements as follows:
config.password_complexity = { digit: 1, lower: 1, upper: 1 }
If you use this code: Devise.friendly_token.first(password_length) to generate the password, you are not always guaranteed to get a password that matches your complexity.
So I wrote a password generator that will respect your password_complexity and will generate a random complaint password:
class PasswordGenerator
include ActiveModel::Validations
validates :password, presence: true, 'devise_security/password_complexity': Devise.password_complexity
attr_reader :password
def initialize
#password = Devise.friendly_token.first(Devise.password_length.first) until valid?
end
end
You can use it as follows:
PasswordGenerator.new.password # "qHc165ku"
(quick caveat: I'm a rails newb)
I tried the generate_token but it doesn't do what you think (look at the docs)
(I'm using rails 3.0.5, and devise 1.1.7)
What I found is that Devise will generate all that stuff for you in the background when you do:
User.create!(:email => "me#example.com", :password => "password")
Devise should create the encrypted_password, and salt for you. (pop open a console and try it out there)

Resources