Ruby on Rails / Devise - Requiring password on email change - ruby-on-rails

This is it, for all the marbles, if I can get this issue solved then I have a project completed.
Anyway, I am using Ruby on Rails 3 with Devise for user authentication. As you may know, in the user admin/edit by default, a user has to enter their current password in the current_password field if they provide a new password. There is a TON of information out there on how to disable current_password so users can change and save freely.
However, I can find very little on doing the opposite: requiring the current password for more fields...in my case, the email field. AND only require the current password when that email addy is changed, not if it remains the same. Currently users can freely change their email without giving their current password, and for security reasons, I don't want this.
After looking through the Devise wiki, I did find this page and I thought I could reverse this code to complete this solution. I managed to work this little bit out in my user.rb model (I stripped out of the unnecessary logic for this post)....
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :name, :email, :password, :password_confirmation, :avatar, :remember_me, :agree
attr_accessor :accessible, :agree, :signed_in
attr_readonly :name
# Validation
validates :name, :presence => TRUE, :uniqueness => TRUE, :length => { :within => 4..20 }
validates :agree, :term_agreement => TRUE, :unless => :signed_in
validates_attachment_size :avatar, :less_than => 1.megabyte
validates_attachment_content_type :avatar, :content_type => ['image/jpeg', 'image/png', 'image/gif']
validates :current_password, :presence => TRUE, :if => :password_required?
protected
def password_required?
email_changed?
end
end
It "almost" works. If I save the user profile with changing nothing, or change other non-password required field (like the user avatar), the profile saves fine, no password required. So far, so good.
And if I change the email address, the validation is triggered....but what happens is that both the current_password AND password (for new password) fields trigger as required. And if I fill in the password in all three (password, password_confirmation, current_password) just for the hell of it, it won't take, just gives a validation error again.
Basically, ONLY the current_password should be required if the email address is changed. How would I make this work in my code?
UPDATE *******
I checked out my log in response to the below suggestion by bowsersenior, and see the following lines when I attempt to save and update the email...
User Load (0.2ms) SELECT `users`.`id` FROM `users` WHERE (LOWER(`users`.`email`) = LOWER('newaddress#changed.com')) AND (`users`.id <> 1) LIMIT 1
User Load (0.2ms) SELECT `users`.`id` FROM `users` WHERE (`users`.`name` = BINARY 'Administrator') AND (`users`.id <> 1) LIMIT 1
SQL (0.1ms) ROLLBACK
I wonder if that 'ROLLBACK' has something to do with the final issue?

Give this a try:
validates :current_password, :presence => TRUE, :if => :email_changed?
I strongly suggest you leave password_required? alone. That could lead to bugs with security and unstable behavior.

I believe the Devise way of doing this is as follows:
class RegistrationsController < Devise::RegistrationsController
def update
#user = User.find(current_user.id)
successfully_updated = if needs_password?(#user, params)
#user.update_with_password(params[:user])
else
# remove the virtual current_password attribute update_without_password
# doesn't know how to ignore it
params[:user].delete(:current_password)
#user.update_without_password(params[:user])
end
if successfully_updated
set_flash_message :notice, :updated
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
private
def needs_password?(user, params)
user.email != params[:user][:email]
end
end
In other words, everything happens at the Controller level and we use either User#update_with_password, when user want to change his email (we know that by invoking needs_password? in the update action) or User#update_without_password, when user changes his other profile data.
Also remember you need to "register" this new RegistrationsController in your routes:
devise_for :users, :controllers => { :registrations => "registrations" }
Source:
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password

Similar to Pawel's answer, but rather than override the controller method update, how about this, based on the devise wiki
class RegistrationsController < Devise::RegistrationsController
protected
def update_resource(resource, params)
if resource.email != params[:email] || params[:password].present?
super
else
params.delete(:current_password)
resource.update_without_password(params)
end
end
end

Related

Rails devise doesn't redirect to root path

My user model looks like this in which type is column in user table and std is used just to get additional data at the time of user sign_up.Now when user(type: Student) login, devise again rendered the login page with message log in successful doesn't render the root_path and in log it is showing rollback transaction but when i refreshed the page then it render to root_path. This problem is only happening for type:Student and when i remove validates_presence_of :std line everything is running perfectly.
Now the question is why this is happening or how it can be done
?
class User < ApplicationRecord
attr_accessor :std
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates_presence_of :type
validates_presence_of :std , allow_blank: false , if: -> { type == 'Student' },
message: 'Student must add grade to continue'
end
Devise::RegistrationsController.rb
class RegistrationsController < Devise::RegistrationsController
def create
super
if params[:user][:type] == 'Student' and user_signed_in?
current_user.grade = Grade.new({cls: params[:user][:std].to_i})
current_user.save
end
end
private
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation, :type, :std)
end
end
Solved.. I think it is also validating user(type:Student)at the time of login so i just added
validates_presence_of :std , allow_blank: false , if: -> { type == 'Student' and new_record? },
message: 'Student must add grade to continue'
Now it is only validating at the time of signup only.

Rails 5 - Devise - When I login it always logs me in as the same user regardless of what credentials are used

Everything was set up fine and seemed to be working. Suddenly I have an issue where if I log out and then log back in again with a different username it just logs me back in always as the first user.
Example:
user one - first#domain.com / password1
user two - second#domain.com / password2
Even if I log out and then log back in again as user two (verified as signed up correctly) it will log me in as user one.
Here is my user.rb file
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
acts_as_voter
has_many :links
has_many :comments
# Virtual attribute for authenticating by either username or email
# This is in addition to a real persisted field like 'username'
attr_accessor :login
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions.to_h).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
elsif conditions.has_key?(:username) || conditions.has_key?(:email)
where(conditions.to_h).first
end
conditions[:email].downcase! if conditions[:email]
where(conditions.to_h).first
end
validates :username, presence: :true, uniqueness: { case_sensitive: false }
validates_format_of :username, with: /^[a-zA-Z0-9_\.]*$/, :multiline => true
validate :validate_username
def validate_username
if User.where(email: username).exists?
errors.add(:username, :invalid)
end
end
end
Application Controller
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
end
end
You might want to check your controllers. If you're determining the logged in user by using anything other than current_user, you may have used the wrong query or perhaps were using a specific user for testing purposes.
Check your User model. You're using :rememberable and might be passing a session cookie without realizing.

Restrict Login with Google OAuth2.0 and Devise to Specific Whitelist Table using Ruby

So I was trying to use omniauth2 to check if the email had the right #domain.com but I think using a database table will allow more functionality as well as being more secure and such.
My previous question: Restrict Login with Google OAuth2.0 to Specific Whitelisted Domain Name on Ruby
I think I want to use a database table to check the email that google authenticated against a whitelist of emails, is there anyway to do this with devise and omniauth2? That way I can say only certain users are authorized after they get authenticated with Google. I have most info listed on my previous question but if there is some more info I can give let me know.
Thanks.
EDIT: Not sure how much this helps but here is a question similar; however, I am still using google and omniauth Whitelisting with devise
EDIT: I think the above "Whitelisting with devise" is pretty close to the answer, but there are still a few kinks to work out. I'm not sure how to start implementing everything I'm pretty new to ruby in particular.
Here is my route:
devise_for :user, :controllers => { :omniauth_callbacks => "user/omniauth_callbacks" }
And that controller:
class User::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
#user = User.find_for_google_oauth2(request.env["omniauth.auth"], current_user)
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
sign_in_and_redirect #user, :event => :authentication
else
session["devise.google_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
If I understand the Whitelisting with devise correctly I have to create another controller in between and use that to check the email? Any help would be greatly appreciated.
EDIT: Here is my user.rb I think this might hold the answer possibly?:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :omniauthable,
:recoverable, :rememberable, :trackable, :validatable, :omniauth_providers => [:google_oauth2]
attr_accessible :email, :password, :password_confirmation, :remember_me, :username, :provider, :uid, :avatar
def self.find_for_google_oauth2(access_token, signed_in_resource=nil)
data = access_token.info
user = User.where(:email => data["email"]).first
unless user
user = User.create(name: data["name"],
email: data["email"],
password: Devise.friendly_token[0,20]
)
end
user
end
end
I'd add a validation to the User Model so, no user would be created if the email that comes from oauth is not form a certain domain:
validates :email,
presence: true,
uniqueness: true,
format: {
message: 'domain must be example.com',
with: /\A[\w+-.]+#example.com\z/i
}

Rails 4/Devise/MongoDB: "Unpermitted parameters" using custom properties and strong parameters

Trying to add a nested custom attribute, Profile (a Mongoid document), to my devise User class. When the Devise registration form is submitted, it should create both a User and a corresponding Profile object as well.
I'd like the end-result to look something like this in my MongoDB:
User:
{
# Devise fields:
"email": "my#email.com",
...
# Custom field
"profile" : "<object_id>"
}
Profile:
{
"first_name": "Dave",
....
}
Unfortunately, I am receiving this in my console whenever I submit my registration. It successfully creates a User but fails to create an associated Profile.
Started POST "/" for 127.0.0.1 at 2013-04-20 23:37:10 -0400
Processing by Users::RegistrationsController#create as HTML
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"awN2GU8EYEfisU0",
"user"=>
{"profile_attributes"=>
{"first_name"=>"Dave",
"birthday(2i)"=>"4",
"birthday(3i)"=>"21",
"birthday(1i)"=>"1933",
"occupation_title"=>"Software Developer"},
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]",
"email"=>"my#email.com"}}
Unpermitted parameters: profile_attributes
I have setup:
Rails 4.0.0beta1, Ruby 2.0.0-p0
Devise ('rails4' branch), Mongoid (from git)
A custom Devise registrations controller to add a definition for strong parameters.
models/user.rb:
class User
include Mongoid::Document
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:token_authenticatable, :confirmable, :lockable, :timeoutable
field :email, type: String, default: ''
...
has_one :profile
accepts_nested_attributes_for :profile
end
models/profile.rb:
class Profile
include Mongoid::Document
include Mongoid::Timestamps
# Attributes
# ----------
field :slug, type: String, default: '' # Acts as user-'friendlier' slug
field :birthday, type: DateTime, default: DateTime.now
field :first_name, type: String, default: ''
field :occupation_title, type: String, default: ''
belongs_to :user
embeds_many :photos
has_one :occupation_industry, :as => :industry
end
controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :profile_attributes)
end
private :resource_params
end
routes.rb
devise_for :users,
:path => '',
:path_names => {
:sign_in => 'login',
:sign_out => 'logout',
:sign_up => 'register'
},
:controllers => {
:registrations => "users/registrations",
:passwords => "users/passwords"
}
I have already looked at these related posts, they didn't seem to help:
Rails 4 Nested Attributes Unpermitted Parameters
https://gist.github.com/kazpsp/3350730
EDIT:
Looks like Devise does actually support strong parameters in its 'rails4' branch (which is supposed to be merged into master in a few days.) Looking through the code, it appears you can override a params function for each action on devise controllers. For creating new users, its sign_up_params instead of resource_params in my example.
Despite changing this name to the proper one, it still didn't work... only whitelisting all parameters using this bang seemed to work:
def sign_up_params
params.require(:user).permit!
end
Obviously, this kind of defeats the purpose of strong parameters... so now the question is how do I permit my nested attributes profile_attributes (as seen in my original question)?
I had the exact same issue and overriding sign_up_params did work for me
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation, :other, :etc)
end
of course, the difference is in that mine are just scalar values, while you're trying to mass assign a relation... I guess that's where you should look for.
By the way, the documentations is still inexistint in this topic (too new), and code commnents suggest to override devise_parameter_sanitizer, which isn't necessary.
I found a different method that allows all the devise overriding logic and code to reside in the application controller. This allows any and all custom params to be passed through for each devise action (sign in, sign up, update). I also add a parameter sanitizer for devise_invitable and handle that logic here (invite, accept_invitation). I've got custom params like avatar, avatar_cache, etc:
#application_controller.rb
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
# There are just three actions in Devise that allows any set of parameters to be passed down to the model,
# therefore requiring sanitization. Their names and the permited parameters by default are:
# sign_in (Devise::SessionsController#new) - Permits only the authentication keys (like email)
# sign_up (Devise::RegistrationsController#create) - Permits authentication keys plus password and password_confirmation
# account_update (Devise::RegistrationsController#update) - Permits authentication keys plus password, password_confirmation
# and current_password. More at https://github.com/plataformatec/devise#strong-parameters
def configure_permitted_parameters
devise_parameter_sanitizer.for(:accept_invitation) do |u|
u.permit(:username,:validate_username, :password,:password_confirmation, :invitation_token)
end
devise_parameter_sanitizer.for(:invite) do |u|
u.permit(:name,:comments)
end
devise_parameter_sanitizer.for(:sign_up) do |u|
u.permit(:username,:password,:password_confirmation)
end
devise_parameter_sanitizer.for(:sign_in) do |u|
u.permit(:username,:email,:password,:password_confirmation,:phone, :validate_username, :avatar_cache, :remove_avatar, :current_password,:remember_me)
end
devise_parameter_sanitizer.for(:account_update) do |u|
u.permit(:username,:email,:password,:password_confirmation,:phone, :validate_username,:avatar, :avatar_cache, :remove_avatar, :current_password)
end
end
Find and read more at https://github.com/plataformatec/devise#strong-parameters
I had the same issue when login, it says: Unpermitted parameters: password, remember_me.
and because i have any controller that inheriting Devise::SessionsController, so i use my own parameter sanitizer.
here is what i do:
Create a file in '#{Rails.root}/lib' fold, my is hzsapa_parameter_sanitizer.rb and required in config/application.rb, then override devise_parameter_sanitizer method in application_controller.rb
lib/hzsapa_parameter_sanitizer.rb
class HzsapaParameterSanitizer < Devise::ParameterSanitizer
def sign_in
default_params.permit(auth_keys + [:password, :remember_me])
end
end
You can override those method depends on your issue:
def sign_in
default_params.permit(auth_keys)
end
def sign_up
default_params.permit(auth_keys + [:password, :password_confirmation])
end
def account_update
default_params.permit(auth_keys + [:password, :password_confirmation, :current_password])
end
config/application.rb
require "hzsapa_parameter_sanitizer"
app/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def devise_parameter_sanitizer
#devise_parameter_sanitizer ||= if defined?(ActionController::StrongParameters)
HzsapaParameterSanitizer.new(resource_class, resource_name, params)
else
Devise::BaseSanitizer.new(resource_class, resource_name, params)
end
end
end
Edit: i just found the solution in devise README, you can follow it here
I used your code and it worked for me!
Here is what I did
class RegistrationsController < Devise::RegistrationsController
skip_before_filter :verify_authenticity_token, :only => :create #, :if => Proc.new { |c| c.request.format == 'application/json' }
respond_to :json, :html, :xml
def create
user = User.new(devise_registrations_permitted_parameters)
if user.save
render :json=> user.as_json(:auth_token=>user.authentication_token, :email=>user.email,:name => user.name), :status=>201
return
else
warden.custom_failure!
render :json=> user.errors, :status=>422
end
end
protected
def devise_registrations_permitted_parameters
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end

Rails devise omniauth: cannot keep facebook data in user session

I am using devise omniauth in my rails application, here is the User class
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable
attr_accessible :email, :password, :password_confirmation, :remember_me, :encrypted_password, :fb_id
def set_facebook_info(info)
#facebook_info = info
end
def get_facebook_info
#facebook_info
end
def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
data = access_token.extra.raw_info
if user = User.find_by_email(data.email)
if not user.fb_id
user.fb_id = access_token.uid
user.save
end
user.set_facebook_info "whatever" <-- I tried here
user
else # Create a user with a stub password.
user = User.create(:email => data.email, :password => Devise.friendly_token[0,20], :fb_id => access_token.uid)
user
end
end
def self.new_with_session(params, session)
super.tap do |user|
user.set_facebook_info "whatever" # <-- I Tried here too
if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["user_hash"]
user.email = data["email"]
end
end
end
end
I want to use set and get methods for keeping some facebook user information. but when I use current_user in view, it gives me no value of what I've set in my User class.
like in application.html.erb :
<span><%= current_user.get_facebook_info %></span>
returns an empty value
Does anybody has an idea about it ? It should be a common case. in general , how can we assign some non connected to DB attribute to current_user via devise ?
Thanks
Your #facebook_info attribute is only stored in memory, not persisted to the database. So on the next page load, Devise is going to load your model from the database again, and that is why the information is missing.
If you want to keep the Facebook info, you need to persist it to the database. Rails has some nice ways of storing hashes directly in a text column:
class User < ActiveRecord::Base
serialize :preferences
end
user = User.create(:preferences => { "background" => "black", "display" => large })
User.find(user.id).preferences # => { "background" => "black", "display" => large }
Source: http://api.rubyonrails.org/classes/ActiveRecord/Base.html

Resources