For my small Rails application, I am using bcrypt to hash users' passwords when they are stored. However, when loading the new user form, I was hit with "invalid hash" for the password, as my new action was
def new
#user = User.new
end
which does not make a new password, which is thus invalid. To remedy this, I tried using
<%= form_for :user, url: users_path do |f| %>
which does not require a user object, allowing me to make that in the create action. However, error handling still needs the User object and throws a nil error
I feel that there should be a "right" way to do this. Can anyone enlighten me?
My user model is as such:
require 'bcrypt'
class User < ActiveRecord::Base
# For user of user.password_hash. Thanks, bcrypt!
include BCrypt
before_save { self.email = email.downcase }
# Validates uniqueness of email
validates_uniqueness_of :email
# Set relationship to lists
has_many :lists
def make_new_password
new_password = Array.new(10).map { (65 + rand(58)).chr }.join
self.password_hash = Password.create(new_password)
end
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
I feel like this book can help you find the right way to do user authentication. (sorry that is the best I can do with the information you have provided).
Hope this helps :)
Related
In my devise sign-up page, i have implemented an ip tracking feature that, when a user signs up, sends the country the user comes from, in order to populate the newly created account's attribute 'user_country'
It works but as a newbie I don't know how to test with rspec that this works i.e that if i create a user who signs up, then AFTER account creation, that user_country is not empty any more.
Here how i use a /app/controllers/registrations_controller.rb method to assign a country to the user's newly created accoun on controllers/registrations_controller.rb- see below
class RegistrationsController < Devise::RegistrationsController
layout 'lightbox'
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete("password")
account_update_params.delete("password_confirmation")
end
#user = User.find(current_user.id)
if #user.update(account_update_params) # Rails 4 .update introduced with same effect as .update_attributes
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
# for Rails 4 Strong Parameters
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :user_country)
end
private :resource_params
protected
def after_sign_up_path_for(resource)
resource.update(user_country: set_location_by_ip_lookup.country) #use concerns/CountrySetter loaded by ApplicationController
root_path
end
end
If it helps, this is how I find the country of a visitor:
module CountrySetter
extend ActiveSupport::Concern
included do
before_filter :set_location_by_ip_lookup
end
# we use geocoder gem
# output: ip address
def set_location_by_ip_lookup
if Rails.env.development? or Rails.env.test?
Geocoder.search(request.remote_ip).first
else
request.location
end
end
end
you need to use doubles in your test.
describe UserController do
it 'sets the request location with Geocoder' do
geocoder = class_double("Geocoder") # class double
location = double('location', country: 'some country') # basic double with return values
expect(geocoder).to receive(:search).and_return(location) # stubbing return value
post :create, user_params
user = assigns(:user)
expect(user.user_country).to eq('some country')
end
end
this way, we create a fake location and geocoder, and also create fake return values. then we assert that the return value from geocoder gets persisted as a user attribute. this way, we don't actually need to test whether Geocoder is working, and simplifies our test setup a lot.
the concepts used here:
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/basics/test-doubles
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/verifying-doubles/using-a-class-double
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/configuring-responses/returning-a-value
if you want to be more robust, you might be able to fake ip address in the test request context, then assert what arguments the fake geocoder is receiving with argument matching: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
in general, you should utilize mocks to isolate your tests' concerns. see https://relishapp.com/rspec/rspec-mocks/v/3-0/docs for more documentation
The prime example of this feature is when creating a user. Instead of saving 'password' to the database you want to take an instance's password and create a password_hash and password_salt from it.
So if I'm creating a form where a user can be created, how can I have a password field if there's no 'password' attribute?
I think previously this could be solved by using attr_accessor in the user model, but I don't know how to do this with strong params:
def user_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
end
Just results in a UnknownAttributes error when trying to create a new instance via the user_params:
#user = User.new(user_params)
If you want to add to your strong params, you can use the .merge method
private
def user_params
params.require(:user).permit(:x, :y, :z).merge(hair_color: "brown")
end
Alternatively, if you're looking to manipulate the data on save, you'd probably use the ActiveRecord callbacks, namely before_create:
#app/models/user.rb
Class User < ActiveRecord::Base
before_create :hash_password
private
def hash_password
if password #-> == self.password
#hash the password
#runs after validations
#runs just before DB insert
end
end
end
I want to call user.skip_confirmation while his account is created by admin in admin panel. I want user to confirm his account in further steps of registration process, but not on create. The only idea I have is to override create in controller:
controller do
def create
user = User.new
user.skip_confirmation!
user.confirmed_at = nil
user.save!
end
end
The problem is, I have different attr_accessibles for standard user and admin, and it works, because ActiveAdmin uses InheritedResources:
attr_accessible :name, :surname
attr_accessible :name, :surname, invitation_token, :as => :admin
It doesn't work after I changed create (it worked before). How can I do what I want and still be able to use this :as => :admin feature?
I look at the answer and none is solving the issue at hand. I solve it the simplest way as shown below.
before_create do |user|
user.skip_confirmation!
end
controller do
def create
#user = User.new(params[:user].merge({:confirmed_at => nil}))
#user.skip_confirmation!
create! #or super
end
def role_given?
true
end
def as_role
# adapt this code if you need to
{ :as => current_user.role.to_sym }
end
end
something like that could work
EDIT: if you define role_given? to return true and as_role, InheritResources will use as_role to get the role information
also
controller do
with_role :admin
end
works, but this way you can't change the role given the user.
At your /app/models/user.rb
before_create :skip_confirmation
def skip_confirmation
self.skip_confirmation! if Rails.env.development?
end
So I have an app in which the users login with their cell phone numbers and get notifications via text/sms. It's a mobile app. I send texts via the applicationmailer by sending emails to "33333333#vtext.com" etc.
However, I have hit a wall with how to override the password reset instructions. I want the message to be sent via text (i don't have their email address), but how do I override devise to do this? I can have the user enter in their number and then do a lookup (i store the contact path as a field in the user, I generate the string in the backend, they don't have to do it).
Ideas?
thanks a bunch!
You can do this by changing your
passwords_controller:
def create
assign_resource
if #resource
#resource.send_reset_password_instructions_email_sms
errors = #resource.errors
errors.empty? ? head(:no_content) : render_create_error(errors)
else
head(:not_found)
end
end
private
def assign_resource
#email = resource_params[:email]
phone_number = resource_params[:phone_number]
if #email
#resource = find_resource(:email, #email)
elsif phone_number
#resource = find_resource(:phone_number, phone_number)
end
end
def find_resource(field, value)
# overrides devise. To allow reset with other fields
resource_class.where(field => value).first
end
def resource_params
params.permit(:email, :phone_number)
end
and then including this new concern in the users model
module Concerns
module RecoverableCustomized
extend ActiveSupport::Concern
def send_reset_password_instructions_email_sms
raw_token = set_reset_password_token
send_reset_password_instructions_by_email(raw_token) if email
send_reset_password_instructions_by_sms(raw_token) if phone_number
end
private
def send_reset_password_instructions_by_email(raw_token)
send_reset_password_instructions_notification(raw_token)
end
def send_reset_password_instructions_by_sms(raw_token)
TexterResetPasswordJob.perform_later(id, raw_token)
end
end
end
which basically uses the private methods that the devise method sent_reset_password_instructions uses adding your own texting logic.
I'm having a problem matching user password using devise gem in rails. User password stored on my db which is encrypted_password and i am trying to find user by password, but I don't understand how to match password from form and encrypted_password in my db.
User.find_by_email_and_password(params[:user][:email], params[:user][:password])
I think this is a better, and more elegant way of doing it:
user = User.find_by_email(params[:user][:email])
user.valid_password?(params[:user][:password])
The other method where you generate the digest from the user instance was giving me protected method errors.
Use Devise Methods
Devise provides you with built-in methods to verify a user's password:
user = User.find_for_authentication(email: params[:user][:email])
user.valid_password?(params[:user][:password])
For Rails 4+ with Strong Params, you can do something like this:
def login
user = User.find_for_authentication(email: login_params[:email])
if user.valid_password?(login_params[:password])
user.remember_me = login_params[:remember_me]
sign_in_and_redirect(user, event: :authentication)
end
end
private
def login_params
params.require(:user).permit(:email, :password, :remember_me)
end
I think the better one will be this
valid_password = User.find_by_email(params[:user][:email]).valid_password?(params[:user][:password])
I would suggest this.
user = User.where("email=? OR username=?", email_or_username, email_or_username).first.valid_password?(user_password)
For 2022, devise is required to add :database_authenticatable to use valid_password? method
class User < ApplicationRecord
devise :xxxable, :yyyable, :database_authenticatable
But, if you need only to verify the entering password just go like this
class User < ApplicationRecord
devise :xxxable, :yyyable#, :database_authenticatable
def valid_password?(verifying_word)
password_digest_instance = BCrypt::Password.new(self.password_digest)
current_password_salt = password_digest_instance.salt
hashed_verifying_word_with_same_salt = BCrypt::Engine.hash_secret(verifying_word, current_password_salt)
Devise.secure_compare(hashed_verifying_word_with_same_salt, self.password_digest)
Then
user = User.find_by(email: params[:user][:email])
user = nil unless user.try(:valid_password?, params[:user][:password])