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.
Related
I am using Devise gem for my application.
Now i would like to create a user using code, but in the database table "users"
the password is encrypted. So i hope i cannot directly save it as
new_user = User.new
new_user.email = "xyz#xys.com"
new_user.password = "1sdf" - i cannot use this becs its actually : encrypted_password
new_user.save
Is this possible?
You should be able to do this actually, devise's encrypted_password method will handle encrypting the password you pass in and storing it in the database
u = User.new(:email => "foo#bar.com", :password => '1sdf', :password_confirmation => '1sdf')
u.save
Try it out in the rails console.
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.
My client wants all user data encrypted, so I've created a before_save and after_find call back that will encrypt certain properties using Gibberish:
# user.rb
before_save UserEncryptor.new
after_find UserEncryptor.new
# user_encryptor.rb
class UserEncryptor
def initialize
#cipher = Gibberish::AES.new("password")
end
def before_save(user)
user.first_name = encrypt(user.first_name)
user.last_name = encrypt(user.last_name)
user.email = encrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
end
def after_find(user)
user.first_name = decrypt(user.first_name)
user.last_name = decrypt(user.last_name)
user.email = decrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
end
private
def encrypt(value)
#cipher.enc(value)
end
def decrypt(value)
#cipher.dec(value)
end
end
Well, when the user first signs up using Devise, the model looks about like it should. But then once the user confirms, if I inspect the user, the first_name and last_name properties look to have been encrypted multiple times. So I put a breakpoint in the before_save method and click the confirmation link, and I see that it's getting executed three times in a row. The result is that the encrypted value gets encrypted again, and then again, so next time we retrieve the record, and every time thereafter, we get a twice encrypted value.
Now, why the heck is this happening? It's not occurring for other non-devise models that are executing the same logic. Does Devise have the current_user cached in a few different places, and it saves the user in each location? How else could a before_save callback be called 3 times before the next before_find is executed?
And, more importantly, how can I successfully encrypt my user data when I'm using Devise? I've also had problems with attr_encrypted and devise_aes_encryptable so if I get a lot of those suggestions then I guess I have some more questions to post :-)
I solved my problem with the help of a coworker.
For encrypting the first and last name, it was sufficient to add a flag to the model indicating whether or not it's been encrypted. That way, if multiple saves occur, the model knows it's already encrypted and can skip that step:
def before_update(user)
unless user.encrypted
user.first_name = encrypt(user.first_name)
user.last_name = encrypt(user.last_name)
user.encrypted = true
end
end
def after_find(user)
if user.encrypted
user.first_name = decrypt(user.first_name)
user.last_name = decrypt(user.last_name)
user.encrypted = false
end
end
For the email address, this was not sufficient. Devise was doing some really weird stuff with resetting cached values, so the email address was still getting double encrypted. So instead of hooking into the callbacks to encrypt the email address, we overrode some methods on the user model:
def email_before_type_cast
super.present? ? AES.decrypt(super, KEY) : ""
end
def email
return "" unless self[:email]
#email ||= AES.decrypt(self[:email], KEY)
end
def email=(provided_email)
self[:email] = encrypted_email(provided_email)
#email = provided_email
end
def self.find_for_authentication(conditions={})
conditions[:email] = encrypted_email(conditions[:email])
super
end
def self.find_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
attributes[:email] = encrypted_email(attributes[:email]) if attributes[:email]
super
end
def self.encrypted_email decrypted_email
AES.encrypt(decrypted_email, KEY, {:iv => IV})
end
This got us most of the way there. However, my Devise models are reconfirmable, so when I changed a user's email address and tried to save, the reconfirmable module encountered something funky, the record got saved like a hundred times or so, and then I got a stack overflow and a rollback. We found that we needed to override one more method on the user model to do the trick:
def email_was
super.present? ? AES.decrypt(super, KEY) : ""
end
Now all of our personally identifiable information is encrypted! Yay!
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.)
I have a User model that acts_as_authentic for AuthLogic's password management. AuthLogic adds "password" and "password_confirmation" attributes over top of the db-backed "crypted_password" attribute. This is pretty standard AuthLogic stuff.
I want to have a method that sets both password and password_confirmation at the same time (useful for internal applications where I'm not worried about typos). To do this I created a new method in User:
#user.rb
def password_and_confirm=(value)
password = value
password_confirmation = value
end
However calling this method does not seem to actually set the password:
user = User.new
user.password = "test"
user.password # => "test"
user.crypted_password # => a big base64 string, as expected
user = User.new
user.password_and_confirm = "test"
user.password # => nil
user.crypted_password # => nil
I also tried a different route:
def internal_password(value)
password = value
end
...and got the same problem.
Why can't I set the password attribute from within a method inside the User class?
Better try this:
#user.rb
def password_and_confirm=(value)
self.password = value
self.password_confirmation = value
end
Otherwise ruby tries to treat the methods (as it is implemented as such) as local variables (this has precedence during assignment operations).