I am using devise gem. Devise send reset password token in mail when user clicks on forget password link. User follow the link and reset his password by entering new password and confirm new password.
When I follow the same mail link again, it again allow the user to reset password in the same way as above.
Now, I want the reset password token to clear once it is used. So that when your follow the previously used send link from old mail, he must get message that "Invalid token"
How can I do this?
Thanks in advance.
Easier and safer solution than what was proposed:
Create your own passwords controller, I chose to place it under controllers/auth
controllers/auth/passwords_controller.rb
class Auth::PasswordsController < Devise::PasswordsController
def update
super do |resource|
if resource.reset_password_token_changed? and resource.reset_password_token_was.nil?
resource.reset_password_token = nil
end
end
end
end
This fix many problem with papertrail for example, and anyhow save one access to the DB
You can try either of the following methods
# reset_password_within = 1.day and reset_password_sent_at = today
reset_password_period_valid? # returns true
# reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
reset_password_period_valid? # returns true
# reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
reset_password_period_valid? # returns false
# reset_password_within = 0.days
reset_password_period_valid? # will always return false
or you can call the instance methods like clear reset password token or by calling
clear_reset_password_token
or after_password_reset methods.
I think this hack should be more easier if you do the following in your user model or the model that has been used by devise.
class YourModel < ActiveRecord::Base
...
def after_password_reset
self.clear_reset_password_token if not (self.reset_password_token.nil? and self.reset_password_sent_at.nil?)
end
end
I suggest not to use your controller to perform business operation. This after_password_reset password is used to called after clear_reset_password token in devise. Here is the reference:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/recoverable.rb#L39
Hope this will help.
I have achieved above by overriding Devise::PasswordsController in application.
Devise handle reset password on PasswordController#edit action.
On edit, I have checked if the reset password token is valid or not. If its valid I allow user to reset password otherwise redirect user to sign in page with "Password token is invalid message".
For devise 3.0
class Users::PasswordsController < Devise::PasswordsController
def edit
self.resource = resource_class.find_or_initialize_with_error_by(:reset_password_token, params[:reset_password_token])
if !resource.errors.empty?
flash[:alert] = "Password token is invalid"
redirect_to new_session_path(resource_name)
end
end
end
For devise 3.1
class Users::PasswordsController < Devise::PasswordsController
def edit
original_token = params[:reset_password_token]
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
self.resource = resource_class.find_or_initialize_with_error_by(:reset_password_token, reset_password_token)
if !resource.errors.empty?
flash[:alert] = "Password token is invalid"
redirect_to new_session_path(resource_name)
end
end
end
Related
I'd like to build a web app based on ruby on rails. For authentication I am using the devise gem. Everything is good : I can create accounts, login, logout etc.
But here I have an issue. I'd like to be able to sign up without providing password but still have the ability to register with password for another account.
I've set the password lenght from 0 to 128 on config/initializers/devise.rb
config.password_length = 0..128
But what are the next steps to do what I want?
Thank you
Ok, I reply here as answer.
Thanks to Ammar Shah i figured out how to get users with password and users without password.
First of all create a file in lib/devise/strategies (create the folders) named database_authenticatable.rb with following code :
require 'devise/strategies/authenticatable'
module Devise
module Strategies
# Default strategy for signing in a user, based on their email and password in the database.
class DatabaseAuthenticatable < Authenticatable
def authenticate!
if password.blank?
authentication_hash[:encrypted_password] = ''
end
resource = mapping.to.find_for_database_authentication(authentication_hash)
hashed = false
if validate(resource){ hashed = true; resource.valid_password?(password) }
remember_me(resource)
resource.after_database_authentication
success!(resource)
end
mapping.to.new.password = password if !hashed && Devise.paranoid
fail(:not_found_in_database) unless resource
end
end
end
end
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
Then in the devise_create_user.rb migration add :
t.string :remember_token
Finally in user.rb model :
before_create :remember_value
def valid_password?(password)
if password.blank?
true
else
super
end
end
def password_required?
new_record? ? false : super
end
def remember_value
self.remember_token ||= Devise.friendly_token
end
Thank you Ammar Shah for helping me !
Reset password length in config/initializers/devise.rb to the default value and use this answer to make password optional.
Also, here is a complete implementation of gradual engagement feature in devise wiki. It depends what exactly you want you achieve.
I am looking to set a secondary password by which I can authenticate a user for a login as from admin. The reason for this work around is the front end is a single page application.
Each user has been given a unique login_as string. now I need to configure Devise to compare the login_as if the password fails.
Any help is appreciated. I am of course open to an alternative solution if there is a better way.
Thanks.
This post from Duncan Robertson was very helpful in solving my issue. I essentially created an override strategy and called it in the devise.rb file. I had some concern regarding tampering with a large user base but it has proved successful. By adding a column to users named ":signin_as" and then setting it to a default unique string with a rake I then had what I needed to fallback on if the initial sign in failed.
the override strategy (config/initializers/auth_override.rb)
module Devise
module Strategies
class AuthOverride < Authenticatable
def custom_auth(user, signin_as)
if user[:signin_as] == signin_as
return true
else
return false
end
end
def authenticate!
user = User.find_by_email(email)
if user
if user.valid_password?(params[:password])
success!(user)
elsif custom_auth(user, params[:password])
success!(user)
else
fail
end
else
fail
end
end
end
end
end
including the strategy in devise (config/initializers/devise.rb)
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :auth_override
end
My app has a pre-subscription system to allow users to subscribe without password.
But malicious users who haven't finished their subscription (= don't have a password yet) could follow the password recovery process to set theirselves a password and brake the usual subscription process.
So I would like Devise to provide password recovery to only the users who are validated (namely users who finished their subscription).
If a non-validated user is entering his email to try to get a new password, I would like to show him the "email not found" error, as if he wasn't found at all.
I didn't find where to override Devise to add such a scope.
This method from the Recoverable seems to fit but I haven't been able to customize it:
# Attempt to find a user by its email. If a record is found, send new
# password instructions to it. If user is not found, returns a new user
# with an email not found error.
# Attributes must contain the user's email
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
So for the moment I overrode the PasswordsController but this is not a clean solution.
class Users::PasswordsController < Devise::PasswordsController
def create
if resource_params[:email].present? && resource_class.registration_complete.where(email: resource_params[:email]).none?
self.resource = resource_class.new
self.resource.errors.add(:email, :not_found)
render :new
else
super
end
end
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])