How to auto-generate passwords in Rails Devise? - ruby-on-rails

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)

Related

Adding JWT to current application

I've been trying to research implementing JWT into my application and a little confused. Currently, I'm using BCrypt in order to Hash and Salt. My file looks like this in the User model
class User < ApplicationRecord
has_secure_password
validates :username, :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
attr_reader :password
after_initialize :ensure_session_token
def self.find_by_credentials(email, password)
user = User.find_by(email: email)
user && user.is_password?(password) ? user : nil
end
def self.generate_session_token
SecureRandom.urlsafe_base64
end
def password=(password)
#password = password
self.password_digest = BCrypt::Password.create(password)
end
def is_password?(password)
BCrypt::Password.new(self.password_digest).is_password?(password)
end
def reset_session_token!
self.session_token = User.generate_session_token
self.save!
self.session_token
end
def ensure_session_token
self.session_token ||= User.generate_session_token
end
end
What I'm trying to ask is would I need to create a new method to further encrypt the password output from using BCrypt? Like I can't find any articles where a user encrypts a user password with BCrypt then uses JWT. All I'm seeing is people mentioning adding BCrypt by adding has_secure_password to the user model and basically creating hashing methods with JWT instead.
My question is replace BCrypt with JWT or what are some recommendation in regards to securing a user password with both JWT and BCrypt? also, any beginner friendly articles would be appreciated.
Thanks for all your help and explanation.
JWT is quite a different way compared to the method of logging in a user and using sessions and cookies to authenticate future requests.
If you think of it like this, user comes to your application and goes to login (like normal, with Devise for example). You receive their username and password on the login and check agasint the BCrypt hash in the database. If they successfully login, you then provide them with a JWT token. Inside this token is encoded their user id.
When they make future requests to your application (normally from an API) then they will provide the token instead of their username password. Your server has a secret and can decrypt this token to check if it is valid and can then use the user_id inside to know that it is the correct user. This will allow them to access any resources that they have access to.
JWT is normally used for javascript front ends or Smart phone apps that want to have long login times (rather than session or cookie). The token is also stateless so as long as the server has the secret, it can check its valid and decrypt it.
Here is a more detailed explanation: https://github.com/dwyl/learn-json-web-tokens
Here is a good guide for setting up JWT with devise: https://medium.com/#mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03

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

How to create a user using code when i am using devise gem for Sign up / Sign in

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.

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