I'm trying to do a custom authentication to better understand what's going on under the hood. It took a while to get it to accept password and password_confirmation, but now it just wont work and I'm all out of ideas
class UsersController < ApplicationController
def new
#user = User.new
end
def create
par = params[:user]
#user = User.new(params[:user])
if #user.verify_password_confirmation(par[:password], par[:password_confirmation]) && #user.save
sign_in #user
redirect_to user_url(#user)
else
render 'new'
end
end
end
Is there any danger to having password and password_confirmation in attr_accessor?
require 'bcrypt'
class User < ActiveRecord::Base
attr_accessor :password_confirmation, :password
attr_accessible :name, :email, :password, :password_confirmation
before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d]\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, length: { minimum: 6 }
validates :password_confirmation, presence: true
def password=(password)
self.password_digest = BCrypt::Password.create(password)
end
def verify_password(password) #for the sessions, which obviously I can't check yet
BCrypt::Password.new(self.password_digest) == password
end
def verify_password_confirmation(pass, pass_con) #couldn't see how else to confirm it
pass_con == pass
end
def reset_session_token!
self.session_token = SecureRandom::base64(32)
self.save!
self.session_token
end
end
EDIT: Specifically the problem is that it's failing at either the #user.save or the verify_password_confirmation and re-rendering
Look at secure_password.rb on github. has_secure_password is much more simple, clear and easy to understand than other third party authentication gems or plugins.
Also note that has_secure_password uses bcrypt-ruby gem for generating password_digest. So if you want to roll your own authenticating system then you will want to think about its security as well.
Related
I have a multistep form, which I created with wizard. Basically the first tep of the form is user/sign_up - which in my understanding not a step yet. After hitting the sign-up button, user moves to the "real" first step, which is :address.
class UserStepsController < ApplicationController
include Wicked::Wizard
steps :address
def show
#user = current_user || User.from_omniauth(request.env["omniauth.auth"])
render_wizard
end
def update
#user = current_user || User.from_omniauth(request.env["omniauth.auth"])
#user.update!(user_params)
render_wizard #user
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :street, :house_number, :city, :zip_code)
end
def redirect_to_finish_wizard(options = nil, params = nil)
redirect_to new_user_profile_path(current_user)
end
end
This is basically the end of the form already. All gets saved to the user. Now I am stuck with validations.
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: %i[facebook]
has_one :profile, dependent: :destroy
after_create :create_profile
accepts_nested_attributes_for :profile
validates :first_name, presence: true
validates :last_name, presence: true
validates :street, presence: true
validates :house_number, presence: true
validates :city, presence: true
validates :zip_code, presence: true
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0, 20]
name = auth.info.name
user.first_name = name.split(" ")[0]
user.last_name = name.split(" ")[1]
end
end
end
I would love to work with the the conditional validations in my model and only validate presence if on a certain step. This should be easy, as I theoretically only have one step, which is address. All I find on the internet, is way too complicated. Question is, do I have to somehow change user/sign_up to a first step in the form and address would be the second step? Or is it fine like this? And if so, can I just add the "if" statements to the address attributes in my validations, somehow defining what is the address step? Would it work like this?
def on_address_step?
wizard.steps = wizard.steps.first
end
Or how do I define it? The validations would look like this then:
validates :first_name, presence: true
validates :last_name, presence: true
validates :street, presence: true, if: :on_address_step?
validates :house_number, presence: true, if: :on_address_step?
validates :city, presence: true, if: :on_address_step?
validates :zip_code, presence: true, if: :on_address_step?
This is surely not that easy. For now this also doesn't work. How do I need to change it? Thanks.
P.S: here is also my Users Controller:
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
redirect_to user_steps_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :street, :house_number, :city, :zip_code)
end
end
If filling in the address is a completely separate process I would just branch the address out into its own model and controller.
class User < ApplicationRecord
# ...
has_one :address
end
class Address < ApplicationRecord
# ...
belongs_to :user
validates :first_name, :last_name, :street,
:house_number, :city, :zip_code, presence: true
end
This avoids turning your user model into even more of a god object and removes the need for the conditional validation that makes your model much more aware of the UX steps than it should be.
# routes.rb
resources :addresses, only: [:new, :create]
class UsersController < ApplicationController
# ...
def create
#user = User.new(params[:user])
if #user.save
session[:user_id] = #user.id
redirect_to new_address_path
else
render :new
end
end
end
class AddressesController < ApplicationController
# You should have some sort of method that checks if the user
# is signed in and redirect otherwise
before_action :authenticate_user!
# GET /addresses/new
def new
# I'm assuming you have some sort of method to fetch the signed in user
#address = current_user.build_address
end
# POST /addresses
def create
#address = current_user.build_address(address_params)
if #address.save
redirect_to '/somepath'
else
render :new
end
end
def address_params
params.require(:address).permit(
:first_name, :last_name, :street,
:house_number, :city, :zip_code
)
end
end
<%= form_with(model: #address) %>
# ... inputs
<% end %>
I doubt you really want the complexity involved with using Wicked which is ok if you really need a long multiple step form but in this case there is a far simpler and better design choice.
I have a Sessions controller that requires authentication for creating a session using .authenticate method of has_secure_password defined in the User model, as per below:
Edit:
class User < ApplicationRecord
before_save { self.email = email.downcase }
# Relationships
has_secure_password
has_many :pedidos
# Validations
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :nome, presence: true
validates :empresa, presence: true
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
validates :cpf, presence: true, length: { minimum: 11, maximum: 14 }
validates :password, presence: true, length: { minimum: 6 }
def admin?
self.admin
end
end
SessionsController
def create
user = User.find_by(email: params[:sessions][:email])
if user && user.authenticate(params[:sessions][:password])
flash[:success] = "Seja bem vindo(a), #{user.nome}!"
session[:user_id] = user.id
redirect_to user
else
flash.now[:danger] = 'Não foi possível realizar o login, '
flash.now[:danger] += 'combinação usuário/senha inválido'
render 'new'
end
end
Now I created an admin/Users controller that I would like to perform database operations without having to enter password and password_confirmation fields. In the current scenario I'm getting error messages saying "password and password_confirmation can't be blank"
How should I proceed? Thank you.
If presumably your problem is that you'd like to be able to save new Users to your db without needing a password, one way might be to just add a dummy password for admins as explained here
Another might be to skip validations when saving an admin to the db. (I haven't tried this with has_secure_password so I'm not positive that it'd work but worth a shot)
Below is my model and controller from which i have filtered unneccessary lines.
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
validates :password_confirmation, presence: true, if: "password.present?"
validates :password, confirmation: true,
length: { in: 6..20}, if: "password.present?"
end
and controller-
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v|
k == "password" or k == "password_confirmation"
}
end
if #user.update(final_params)
redirect_to #user
else
render 'edit'
end
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password,:password_confirmation, :mobile, :country, :state, :city, :pincode)
end
end
the problem is when updating a data, it shows a validation error i.e password confirmation can not be blank. even if I enter something to that field and submit. and to find error i tried replacing "password.present?" from password confirmation validation with "password.exists?" and it showed exception that exists is not a valid method for "123456 : string" . 123456 is the current password in DB. why is it checking password against db ? and please help me to solve this.
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v| k == "password" or k == "password_confirmation"}
end
Your problem is the first line here... your params are params[:user][:password] not params[:password] (you can see that in your get_params method)
So always your code is going to run the section that removes the password/confirmation
Also:
validates :password_confirmation, presence: true, if: "password.present?"
using a string of ruby in the validation is not generally considered good practice. How about adding a method like so:
validates :password_confirmation, presence: true, if: :confirmation_needed?
def confirmation_needed?
password.present?
end
Finally, you also need to not check the length of password_confirmation if it hasn't actually been entered:
validates :password, confirmation: true, length: { in: 6..20},
allow_blank: true, if: :confirmation_needed?
It's never a good idea to chunk down the incoming parameters in the controller.
Rather, putting proper validations in the model is a good idea !
hence cleaned up your controller.
Check below code:
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
redirect_to #user and return if #user.update_attributes(get_params)
# render will not be executed if the user is redirected & returned
render :edit
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password, :password_confirmation, :mobile, :country :state, :city, :pincode)
end
end
modified model:
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
# above validation will be effective only for during new record creation.
# below 2 validations will be cheked only if password is present in the params list.
validates :password, confirmation: true,
length: { in: 6..20 }, if: validate_password?
validates :password_confirmation, presence: true, if: validate_password?
private
def validate_password?
password.present?
end
end
if still this does not help, then try to debug the self object in the validate_password? method. use raise self.inspect in the validation method to verify the incoming parameters.
That way you can track where you are going wrong.
I have a edit form for a client. It is also possible to change the password, but of course you don't want to change(and/or reenter) your password every time you change on of the other settings. To avoid updating the password I tried this:
def client_update_params
if admin? == true
params.require(:client).permit(:name, :email,:company_name,
:address_street,:address_number,:address_city,
:address_zip,:address_country,:billing_informations)
else
if params[:client][:password].blank?
params[:client].delete("password")
params[:client].delete("password_confirmation")
params.require(:client).permit(:name, :email,:company_name,
:address_street,:address_number,:address_city,
:address_zip,:address_country)
else
params.require(:client).permit(:name, :email,:company_name,
:address_street,:address_number,:address_city,
:address_zip,:address_country,
:password,:password_confirmation)
end
end
end
So the idea is to check if the password field is set or not. If it is set, update with new password, else do not update the password. But every time I hit submit(and leave the password field empty), the form validation says the password is to short....
Is there maybe a working/more elegant solution for this problem ?
EDIT:VALIDATIONS ON MODEL:
attr_accessor :is_admin_applying_update
attr_accessor :plain_password
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
before_save { self.email = email.downcase }
before_create :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :company_name,presence:true
validates :address_street,presence:true
validates :address_number,presence:true
validates :address_city,presence:true
validates :address_zip,presence:true
validates :address_country,presence:true
validates :billing_informations,presence:true
has_secure_password
validates :password, length: { minimum: 6 }, :unless => :is_admin_applying_update
def Client.new_remember_token
SecureRandom.urlsafe_base64
end
def Client.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = Client.encrypt(Client.new_remember_token)
end
Remember that you don't really have a password attribute in your model. The password is stored encrypted in a field named password_digest.
You should only be validating the password attribute when a password and a password_confirmation is given. You can do this in your model validation rather than deleting things from the params hash.
Also, you should validate the existence of a password_confirmation if password is present.
Add a virtual attribute skip_password like the devise uses
In model
attr_accessible :skip_password
validates :password, length: { minimum: 6 }, :unless => :skip_password
def skip_password=(value)
#skip = value
end
def skip_password
#skip
end
In controller
#client.skip_password = true
I just came across omniauth-identity which enables users to sign in and register without using a Facebook, Twitter, etc.
There is a step were you have to create a Identity model (I'm following this Railscast):
class Identity < OmniAuth::Identity::Models::ActiveRecord
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, presence: true, length: { minimum: 6 }
validates :password_confirmation, presence: true
end
Now, I already have an User model and a login and registration system (created by following the Ruby on Rails Tutorial):
user.rb:
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, presence: true, length: { minimum: 6 }
validates :password_confirmation, presence: true
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
So I'm a bit confused. Should remove the lines that have to do with authentication in the User model (e.g. validation, attr_accesible, create_remember_token etc. along with the name and email fields in the users table)?
And remove sessions_helper.rb too?
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
def store_location
session[:return_to] = request.url
end
end
Because, correct me if I'm wrong, but I I think omniauth-identity handles that too (except for the current_user part.
Creating the Identity model is useful mostly to authenticate with multiple providers. Here's a good description of how to go about doing that:
https://github.com/intridea/omniauth/wiki/Managing-Multiple-Providers
It answers your question what should be in the User model and what in the Identity model.