I'm writing an application for myself, so I've got no rush and the my only target is to do things properly.
For authentication I use devise, but I turned out customizing it a lot.
I've seen some good features coming in Rails 3.1 that could make easier to implement auth myself.
In general, when does Devise stops to be useful and starts getting in your way?
Here is a list of customization I have at the moment, beside views of course, but I still would like to implement at least SEO friendly urls.
# model/User.rb
#this method is called by devise to check for "active" state of the model
def active?
#remember to call the super
#then put our own check to determine "active" state using
#our own "is_active" column
super and self.is_active?
end
protected #====================================================================
# find in db the user with username or email login
def self.find_record(login)
where(attributes).where(["name = :value OR email = :value", { :value => login }]).first
end
# allow no case sensitive email
# (saved downcase, fetched downcase)
def self.find_for_authentication(conditions)
conditions[:email].downcase!
super(conditions)
end
# find the user in the db by username or email
def self.find_for_database_authentication(conditions)
login = conditions.delete(:login)
where(conditions).where(["name = :value OR email = :value", { :value => login }]).first
end
# Attempt to find a user by it's email. If a record is found, send new
# password instructions to it. If not user is found, returns a new user
# with an email not found error.
def self.send_reset_password_instructions(attributes={})
recoverable = find_recoverable_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
def self.find_recoverable_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
case_insensitive_keys.each { |k| attributes[k].try(:downcase!) }
attributes = attributes.slice(*required_attributes)
attributes.delete_if { |key, value| value.blank? }
if attributes.size == required_attributes.size
if attributes.has_key?(:login)
login = attributes.delete(:login)
record = find_record(login)
else
record = where(attributes).first
end
end
unless record
record = new
required_attributes.each do |key|
value = attributes[key]
record.send("#{key}=", value)
record.errors.add(key, value.present? ? error : :blank)
end
end
record
end
# password not required on edit
# see: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
def password_required?
new_record?
end
# controllers/registrations_controller.rb
# devise controller for registration
class RegistrationsController < Devise::RegistrationsController
# update_attributes (with final S) without providing password
# overrides devise
# see: https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password
def update
# Devise use update_with_password instead of update_attributes.
# This is the only change we make.
if resource.update_attributes(params[resource_name])
set_flash_message :notice, :updated
# Line below required if using Devise >= 1.2.0
sign_in resource_name, resource, :bypass => true
redirect_to after_update_path_for(resource)
else
clean_up_passwords(resource)
render_with_scope :edit
end
end
end
Thank you
I'd just stick with devise for the time being, your changes aren't huge. However, I'd fork devise and extract the changes you've made into new features. Then attempt to get them pulled into devise itself. That way maintaining them doesn't fall on you, it can fall on the many.
Maintaining a full authentication system can be a real headache and ultimately its reinventing the wheel. It only takes one mistake can leave you wide open.
Also your new find_for_authentication method, this has now been supported in devise, put in your devise initializer...
config.case_insensitive_keys = [ :email ]
Good question - My view would probably be that as long as it makes things easier it's useful. You can always fork devise on github and put your customisation in there to some extent - which is what I've done on one project. I'm also a bit nervous about rolling my own authentication when it can be so important to get it right, especially if other people want to see stuff they shouldn't. But I'll be interested to see what others think.
Related
In my application I am trying to manually make devise sessions unusable by setting a session_validity_token.
How I do it:
The devise User model has a column named session_validity_token
I also have a SeesionHistory model which has the same column
In my devise initialization ...
Warden::Manager.after_set_user except: :fetch do |user, warden, opts|
user.update_attribute(:session_validity_token, Devise.friendly_token) if user.session_validity_token.nil?
warden.raw_session["validity_token"] = user.session_validity_token
end
Warden::Manager.after_fetch do |user, warden, opts|
unless user.session_histories.unblocked.where(session_validity_token: warden.raw_session["validity_token"]).exists?
warden.logout
end
end
... when a user signs in or up I set the validity_token of the stored Cookie to the Users session_validity_token. If the User doesn't have one yet (signup), I create a token.... when a URL gets fetched I check before authorizing the User if a unblocked session to that token exists. If not, the User gets logged out.
In the ApplicationController ...
def after_sign_in_path_for(resource_or_scope)
session = SessionHistory.create(user_id: current_user.id, session_validity_token: current_user.session_validity_token)
current_user.update_attribute(:session_validity_token, Devise.friendly_token)
request.env['omniauth.origin'] || stored_location_for(resource) || root_path
end
... after a User gets signed in, I create a SessionHistory Record and simply set it's session_validity_token to the Users token and then recreate the Users token.
Unfortunately I get the following error:
NoMethodError in Users::SessionsController#create
undefined method `session_validity_token' for nil:NilClass
Here is the SessionController#Create Action:
def create
if User.where("email = '#{params[:user][:login]}' or username = '#{params[:user][:login]}'").exists?
#user = User.find(User.where("email = '#{params[:user][:login]}' or username = '#{params[:user][:login]}'").first.id)
if #user.confirmed? || ((Time.now - #user.created_at).to_i / (24 * 60 * 60)) < 1
super
else
redirect_to new_user_confirmation_path(q: "unconfirmed")
end
else
flash[:alert] = "The email or username does not match any accounts"
redirect_to new_user_session_path
end
end
So I guess I did something wrong when handling the tokens with Warden ...
Please ask if you need additional Information.
You may have a namespace collision between two customizations named session_validity_token. This is not naturally in the Devise model (and is not in the source for devise--I checked that).
If that is the case, and you have power over the source, consider changing the name of one, or both of the session_validity_token symbols to clarify the specific usage and relieve the conflict.
This is from the github page:
require 'bcrypt'
class User < ActiveRecord::Base
# users.password_hash in the database is a :string
include BCrypt
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
It appears that to access the password method, you need to call it as an attribute from a create method:
#user.password = user_params[:password]
#user.save
Okay...fine? But where's the salt now stored? I just don't get it at all, how is this even remotely secure anymore?
To retrieve a hashed password, you need this method:
def password
#password ||= Password.new(password_hash)
end
And call it as an attribute:
if #user.password == params[:password]
give_token
else
false
end
So it appears everything's working without a salt...how does it do this?
This means I only need one column in my database to do with passowords now, right?password or password_hash instead of password_salt | password_hash?
Well then why does the github page say this:
But even this has weaknesses -- attackers can just run lists of
possible passwords through the same algorithm, store the results in a
big database, and then look up the passwords by their hash:
PrecomputedPassword.find_by_hash(<unique gibberish>).password #=> "secret1"
Salts
And then this is what really gets me:
The solution to this is to add a small chunk of random data -- called a salt -- to the password before it's hashed:
Why are they explaining all of this if bcrypt is handling everything automatically?
hash(salt + p) #=> <really unique gibberish>
The salt is then stored along with the hash in the database, and used to check potentially valid passwords:
<really unique gibberish> =? hash(salt + just_entered_password)
bcrypt-ruby automatically handles the storage and generation of these salts for you.
Could someone explain how bcrypt stores and generates these salts? Why does it say it handles it all for me and then goes on to tell me how to generate a salt? Do I need to run something like this in my model: self.password_hash = hash(salt + p)
Argh so confused I used to get salts and hashes utterly and now they've changed it all. Terrible, unclear docs...they appear to show you how to use bcrypt without a salt with loads of examples, and then briefly mention how to do it properly with a salt down at the bottom.
Could someone please give me an example how to use the new version of bcrypt to generate a salt and hash, and how to authenticate?
Okay, the has_secure_password is really cool. You don't need to worry about salts and hashes anymore, the salt and hash are stored as one attribute ( password_digest) in the database.
It's saved in such a way that bcrypt knows which part of the password_digest string is the salt, and what is the hash.
If you're setting up authentication from scratch, you literally need to do the following:
1) Add the bcrypt rails gem:
gem bcrypt-rails
2) Add the has_secure_password method to the model tasked with handling your user records:
class User < ActiveRecord::Base
has_secure_password
end
3) Make sure your users table has a password_digest column:
class CreateUser < ActiveRecord::Migration
create_table :users do |t|
t.username
t.password_digest
end
end
4) Create a new method to create a a new empty user instance for the form to use:
class UsersController < ApplicationController
def new
#user = User.new
end
end
5) In the new view, make a form that creates populates the params hash' :password and :username entries:
<% form_for( #user ) do |f| %>
<%= f.text_field :username %>
<%= f.password_field :password %>
<% end %>
6) Back in our controller, permit the username and the password using strong params. The whole reason behind strong params is to prevent some cheeky chappy from using dev tools to create their own html form field (such as one pertaining to id) and populating the database with malicious data:
class UsersController < ApplicationController
def new
#user = User.new
end
private
def user_params
params.require(:user).permit(:username, :password)
end
end
7) Let's create the create method that will use these permitted entries to create a new user, populated by the form:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
#user.save
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:username, :password)
end
end
Set up your routes as you see fit, and that's it! The user record's password_digest column will be automatically populated with one string comprised of a salt appended with the hash of the password. Very elegant.
All you need to remember: password -> password_digest.
In order to authorise the user and signout the user, create a sessions controller with a create method and a destroy method:
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to admin_root_path, :notice => "Welcome back, #{user.username}"
else
flash.now.alert = "Invalid email or password"
redirect_to root_path
end
end
def destroy
reset_session
flash[:info] = "Signed out successfully!"
redirect_to root_path
end
Hope this helps someone!
bcrypt got everything covered for you. Your password digest consists of a few types of information, bcrypt algorithm type, cost, salt and the checksum.
for example:
my_password = BCrypt::Password.create("my password")
#=> "$2a$10$.kyRS8M3OICtvjBpdDd1seUtlvPKO5CmYz1VM49JL7cJWZDaoYWT."
The first part: $2a$ is the variant of the algorithm see: Where 2x prefix are used in BCrypt?
The second part 10 is the cost parameter, you can increase it to slow down the process (logarithmic value) by providing a hash {cost: 12} as the second argument to create.
Now if you call my_password.salt you get "$2a$10$.kyRS8M3OICtvjBpdDd1se" which identifies the part that is being used as the key to creating your checksum.
And finally, your checksum is "UtlvPKO5CmYz1VM49JL7cJWZDaoYWT.". That's the reason if you call create the second time the string is going to be different as another salt will be used.
But as I mentioned earlier you don't need to do anything extra as all these are being taken care of for you.
I'm trying to add authentications controller for my current devise system, in order to provide multiple logins with facebook and twitter.
To do that, I'm following this tutorial: http://railscasts.com/episodes/236-omniauth-part-2
My problem is, for the person, who hasn't registered yet, and trying to register with twitter.
So I need to create both user and authentication for that.
My code is the following:
user = User.new
token = omni['credentials'].token
token_secret = omni['credentials'].secret
user.provider = omni.provider
user.uid = omni.uid
user.authentications.build(:provider => omni['provider'], :uid => omni['uid'], :token => token, :token_secret => token_secret)
if user.save
flash[:notice] = "Logged in."
sign_in_and_redirect(:user, user)
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_path
end
So at the end of the registration process, the new user is created. However in the database, I don't see any twitter authentication record with respect to that user.
Is that because of the user.authentications.build ?
That would be great if you can help me.
Thanks.
As a data point: The railscasts you're referring to references Omniauth pre-1.0, which had a slighly different strategy than what that railscsts reference. (Note: I'm using the exact method you're referencing on a live site ). In this case, the build calls "apply_omniauth" -
Make sure you've created (as they reference in the video), a registrations controller which builds the resource. Here is my current working example:
class RegistrationsController < Devise::RegistrationsController
def create
super
session[:omniauth] = nil unless #user.new_record?
end
private
def build_resource(*args)
super
if session[:omniauth]
# apply omniauth calls the user model and applies omniauth session to the info
#user.apply_omniauth(session[:omniauth])
#
#user.valid?
end
end
end
However, you still need to create the authentication record, here is my exact call:
current_user.authentication.create!(:provider => omniauth['provider'], :uid => omniauth['uid'])
Hope it helps.
Yes, it is because of build
User.build # allocates a new record for you
User.create # allocates and then saves a new record for you
So I think you want
user.authentications.create(:provider => omni['provider'],
:uid => omni['uid'],
:token => token,
:token_secret => token_secret)
In addition, you should handle the case where the create does not save (validation problem)
I suppose if you are using Devise+Omniauth , you could take a look at this more recent Railscast. There is a native support of OmniAuth in the new version of Devise gem .
Yes it is because of build, it is use to build a record without saving it in the database (like new).
If in your model you have a User has_many :authentications , you can set the autosave option to true to automatically save the authentications when you are saving the user :
has_many :authentications, autosave: true
I am looking for a customization in devise where if we click on forgot password it should send the mail to any e-mail id . Something like it happens in Gmail, irrespective of the email id exists or not.
Screen 1
Screen 2
Currently what i have is this in which it tries to validate with the valid users in the system.
The Devise, recoverable module takes care of this
def send_reset_password_instructions(attributes={})
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
recoverable.send_reset_password_instructions if recoverable.persisted?
recoverable
end
How can i remove this validation and have email sent to any Email id?
There is a Devise configuration called paranoid that, when set to true, would change the message in a way to avoid e-mail enumeration. Just set config.paranoid = true in your Devise configuration.
My solution would be to extend/override Devise's passwords controller. To do this, create a controller (let's call it passwords) that inherits from Devise's passwords controller, like this:
class PasswordsController < Devise::PasswordsController
Then edit your routes file so this change takes effect:
devise_for :users, :controllers => { :passwords => 'passwords' }
Now, you'll want to override the create action. There are several ways you could do this but since I'm not sure of what you want to do, I'll show you 2 things you could do:
You only want to prevent the "Email not found" error so that people can't find which emails exist or not in your database:
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name))
end
You really want to send emails to any entered email:
def create
self.resource = resource_class.send_reset_password_instructions(resource_params)
unless successfully_sent?(resource)
Devise::Mailer.reset_password_instructions(resource).deliver
end
respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name))
end
Now, the problem with this last solution is that you are sending an email to a user that doesn't exist... And so when the user comes back, he won't be able to enter his new password since his user account can't be found. But if you really want to do this, hopefully I set you on the right track.
I'm adding a password reset feature to my Rails application that uses Authlogic. I was following the guide here: http://www.binarylogic.com/2008/11/16/tutorial-reset-passwords-with-authlogic/ and everything works as I'd like except for one thing: the password reset form accepts blank passwords and simply doesn't change them.
I've been searching around, and have learned that this is the intended default behavior because it allows you to make user edit forms that only change the user's password if they enter a new one, and ignore it otherwise. But in this case, I specifically want to enforce validation of the password like when a user initially registers. I've found two possible solutions for this problem but haven't been able to figure out how to implement either of them.
1) Someone asked this same question on Google Groups:
User model saves with blank password
Ben's response was to use #user.validate_password = true to force validation of the password. I tried this but I get an undefined method error: undefined method 'validate_password_field=' for #<User>.
2) There seems to be an Authlogic configuration option called ignore_blank_passwords. It is documented here:
Module: Authlogic::ActsAsAuthentic::Password::Config#ignore_blank_passwords
This looks like it would work, but my understanding is that this is a global configuration option that you use in your initial acts_as_authentic call in the User model, and I don't want to change it application-wide, as I do have a regular edit form for users where I want blank passwords to be ignored by default.
Anyone found a solution to this? I see validate_password= in the change log for Authlogic 1.4.1 and nothing about it having been removed since then. Am I simply using it incorrectly? Is there a way to use ignore_blank_passwords on a per-request basis?
This is kind of an old thread, but since it is unanswered I'll post this.
I've managed to do it a bit more cleanly than the other solutions, "helping" authlogic validations with my own.
I added this to user:
class User < ActiveRecord::Base
...
attr_writer :password_required
validates_presence_of :password, :if => :password_required?
def password_required?
#password_required
end
...
end
You can reduce it to two lines by making an attr_accessor and using :if => :password_required (no interrogation), but I prefer this other syntax with the interrogation sign.
Then your controller action can be done like this:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
#user.password_required = true
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
This will have a local effect; the rest of the application will not be affected (unless password_required is set to true in other places, that is).
I hope it helps.
This what I did.
class User < ActiveRecord::Base
attr_accessor :ignore_blank_passwords
# object level attribute overrides the config level
# attribute
def ignore_blank_passwords?
ignore_blank_passwords.nil? ? super : (ignore_blank_passwords == true)
end
end
Now in your controller, set the ignore_blank_passwords attribute to false.
user.ignore_blank_passwords = false
Here, you are working within the confines of AuthLogic. You don't have to change the validation logic.
User.ignore_blank_passwords = false
Use model, not object for setting this property.
def update_passwords
User.ignore_blank_passwords = false
if #user.update_attributes(params[:user])
...
end
User.ignore_blank_passwords = true
end
Maybe test the value of the parameter in the controller? (air code):
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.password.blank?
flash[:error] = "Password cannot be blank"
render :action => :edit
return
end
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
Apart from zetetic's solution you could do it this way:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.changed? && #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
You're basically checking if authlogic changed the user record (which it doesn't if the password is empty). In the else block you can check if the password was blank and add an appropriate error message to the user record or display a flash message.