Devise: Allow users to register as "UsErNaMe" but login with "username" - ruby-on-rails

In the same way most websites work, I was to store "UsErNaMe" in the database but let users login with "username".
This is a fairly obvious and necessary feature, and plenty of people seem to have asked it, but the solution I keep stumbling upon seems disconnected from Devise's own documentation.
For instance, consider this blog post: http://anti-pattern.com/2011/5/16/case-insensitive-keys-with-devise
[...]you’ve probably run into the problem that some users like to type
certain letters in their logins (email and/or username) in uppercase,
but expect it to be case-insensitive when they try to sign in. A not
unreasonable request[...]
Cool! That's what I want.
His solution:
# config/initializers/devise.rb
Devise.setup do |config|
config.case_insensitive_keys = [:email, :username]
end
That's the solution I keep finding. But here's the documentation for that config option:
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
config.case_insensitive_keys = [ :username, :email ]
In particular: "These keys will be downcased upon creating/modifying a user." In other words, the username is being downcased in the database.
To verify:
User.create username: "UsErNaMe", password: "secret", email: "email#com.com"
#=> <User username="username"...>
Am I missing something painfully obvious?

From devise wiki: you need to overwrite devise's find_first_by_auth_conditions method in your model.
ActiveRecord example:
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
where(conditions).first
end
end
You can remove OR lower(email) = :value part if you don't need auth by email too.
That way you don't need to list username in case_insensitive_keys and it wouldn't be downcased in the database.

Related

How to customize Devise to use username as authentication key instead of email on only one user type?

I have two user types, admins and customers, I've been trying to make customers login with their usernames exclusively, while keeping admins unaffected(i.e., still login with their emails). I've gone through the process of dropping the email on login/register for the customers. But it turns out that the process is affecting the admins too -- they cannot login with their emails anymore.
The problem is that I couldn't find a way to change devise's configuration options at runtime from
Config.authentication_keys = [ :username ] #For customers
to:
Config.authentication_keys = [ :email ] #For admins
based on namespace.
Is it possible to do such a thing?
P.S: I tried injecting Proc.new {#block} to Config.authentication_keys to add some logic to the configuration option, but it's failing due to Devise expecting the option to be an array.
For example:
config.authentication_keys = Proc.new { [ :email ] }
fails when sanitizing parameters with devise_parameter_sanitizer.for(:sign_up) << [..] with this error
undefined method `+' for #<Proc:0x5d872a0>
You can override your find_first_by_auth_conditions method for User model:
# app/models/user.rb
def self.find_first_by_auth_conditions(conditions, opts={})
if admin?
find_by(email: conditions[:login])
else
find_by(username: conditions[:login])
end
end
# config/initializers/desvise.rb
Devise.setup do |config|
config.authentication_keys = [:login]
P.S.: Example for mongoid, I don't rememeber how to call find for AR.

Devise and User.create

Usually when we create a model, say User, its attributes match with database fields.
For example, if my corresponding database table users_development has fields name and score then when I create an instance of the User class I simply type user = User.create(:name => "MyName", :score => 85).
Now, Devise created a migration file including fields email and encrypted_password, but I cannot see the field password (which is quite logically from the security point of view).
While looking through forum posts I saw many examples like User.create(:email =>"me#domain.com", :password => "foo"). So, where did the password come from? It is not a field of table users_development. What is going on behind the scene? I looked through the documentation on http://rubydoc.info/github/plataformatec/devise/master/Devise but couldn't find any explanation.
User.create(:email => "me#domain.com", :password => "foo") does not directly create a database record with those exact fields. Rather, it uses public_send("#{k}=", v) for each pair in the parameters hash. So really, it's doing something like this internally:
user = User.new
user.email = "me#domain.com"
user.password = "foo"
user.save
Even though you don't have a password database field, Devise's DatabaseAuthenticatable module adds a password= method, which updates the encrypted_password field:
def password=(new_password)
#password = new_password
self.encrypted_password = password_digest(#password) if #password.present?
end
This method from devise source code does the trick :
# Generates password encryption based on the given value.
def password=(new_password)
#password = new_password
self.encrypted_password = password_digest(#password) if #password.present?
end
When calling create, update attributes, build, etc, rails will try to call for each field the method field=, so when you pass :password => 'foo' to create it will do something like :
user = User.new
user.password = 'foo'
user.save
Here this method allows to build the model with an unhashed password but to store the hashed password in the database.

How does omniauth-ldap work?

I'm currently working on a rails project in which I am in charge of user authentication.
We've decided to use third party authentication and I tried following an example setup.
The example is done by Kevin Thompson and is called example.
According to the LDAP sever's documentation, the steps I need to do are:
Connect to the LDAP server.
Bind anonymously (no DN and password).
Search for the LDAP entry using the username
Retrieve the DN for the username if found.
Rebind with the user's DN and password that they supplied.
If this rebind succeeds, the user is authenticated.
I've followed Thompson's example, except that I'm not using nifty; using devise for user management and omniauth-ldap for authentication. However, it's not quite working, and I'm wondering if it has to do with a discrepancy between what the server documentation tells me to do and what omniauth-ldap is actually doing...
Specifically, my problem is that I always get an "Invalid credentials" error.
Is this because of a mismatch between what I need to do and what omniauth-ldap is doing?
Advice or suggestions are greatly appreciated!
A little more information about how I've set up (to maintain anonymity, I replaced some things)
I can post more of my code upon request.
config/initializers/devise.rb:
config.omniauth :ldap,
:host => 'ldap1.its.domain.ext',
:base => 'ou=People, dc=domain, dc=ext',
:port => 389,
:attrs => 'uid',
:method => :plain,
:uid => 'uid'
app/controllers/users/omniauth_callbacks_controller.rb:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :verify_authenticity_token
def ldap
ldap_return = request.env["omniauth.auth"]["extra"]["raw_info"]
username = ldap_return.uid[0].to_s
if #user = User.find_by_username(username)
sign_in_and_redirect #user
else
#user = User.create(:username => username,)
sign_in_and_redirect #user
end
end
end
I just solved a similar issue.
First, you'll need to determine if your domain allows anonymous binding. This was not allowed in my case. There is an great pull request for using the current user to bind, dorren/omniauth-ldap. Otherwise, you'll need a system account. Just to get moving initially, I used my username(i.e., userPrincipalName)/password for :bind_dn and :password.
Secondly, for LDAP authentication, the two uid values that are used to authenticate are sAMAccountName(username) or userPrincipalName(username#ldap.domain.ext). I found that my system uses userPrincipalName. To prevent the user from entering that in, I just concatenated the domain prior to submitting the form.
Try this config.
config.omniauth :ldap,
:host => 'ldap.domain.ext',
:base => 'dc=ldap, dc=domain, dc=ext',
:port => 389,
:method => :plain,
:uid => 'userPrincipalName',
:bind_dn => 'bind_dn',
:password => 'password'
I believe :bind_dn can be of the form:
'CN=LastName\, FirstName, OU=People, DC=ldap, DC=domain, DC=ext'
OR
'username#ldap.domain.ext'
I also found writing a ruby script using Net::LDAP, to bind and search really helped me learn about Active Directory as well, since I had no knowledge of this subject prior to this task.
I can't add this as a comment, as I don't have 50 reputation yet, however I found this which may be of use to some people here.
http://blackfistsecurity.blogspot.com.au/2011/12/rails-authentication-using-devise-and.html
I was originally trying to use Omniauth and Omniauth-LDAP without anything else, but the lack of documentation on omniauth-ldap's part makes things difficult.
edit:
Instead of using Omniauth-LDAP, I ended up opting for a vanilla devise install, and wrote my own LDAP functionality. Please note: I use mongoid, and as such the code below is directed towards MongoDB. It can be easily modified for ActiveRecord, however.
In order to do this, I edited the new action in the sessions controller, similarly to as follows:
ldap = Net::LDAP.new
ldap.host = 'domainOrIP'
ldap.port = 389
ldap.auth 'user', 'password'
if ldap.bind
# success, so let's check if the user exists
#existing_user = User.where({username: params[:user][:username] }).first
if #existing_user == nil
#create the user
#user = User.new( {username: params[:user][:username], password: ''})
# I didn't personally store the user's password, as I use LDAP for authentication. (If you save this, please hash and salt it first!!)
#user.save
flash[:notice] = "Success!"
redirect_to '/'
else # already existed
#user = User.find({ username: params[:user][:username] })
flash[:notice] = "Success!"
redirect_to '/'
end
else
flash[:danger] = "An error occurred whilst authenticating with your LDAP server. Please check the configuration and try again."
redirect_to '/'
end
I then left Devise to handle everything else. Worked brilliantly for me -- the code above is from memory, however, so may not be 100% accurate. :)
edit 2:
More information on how to use the Net::LDAP class can be found here:
http://www.rubydoc.info/gems/ruby-net-ldap/Net/LDAP

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)

RoR Devise: Sign in with username OR email

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.)

Resources