I decided to customize(override) Devise's session controller to implement my own sign in logic. I've left the sessions#new controller alone and simply call super in it.
def new
super
end
However, I have removed the call to super in my custom sessions#create method so that I can send an HTTP request to a server for authentication. Subsequently, I either sign in the user or return the user to the sessions#new path to try their login again.
def create
#member = Member.find_by_username(params[:username])
response = authenticate_with_api
if response[:authenticated]
if #member
#member.update(response.[:member])
sign_in #member
else
#member.create(response.[:member])
sign_in #member
end
else
flash[:alert] = "Incorrect username or password"
flash[:error] = "Incorrect username or password"
flash[:notice] = "Incorrect username or password" #setting all the messages out of frustration!
redirect_to new_member_session_path
end
The logic flow is working correctly, however the flash messages are not displaying. I have set a flash message just before calling super in the new method to ensure that the view was setup properly:
def new
flash[:notice] = "Test flash message"
super
end
This works. I can see the flash message appear in the template. The only explanation I can come up with is that somehow, there are multiple redirects occurring. However, when I look in the logs, I can only see the redirect I specified. I have also tried using keep to persist the flash message through multiple requests, but even that does not work:
flash.keep[:notice] = "Test flash message"
redirect_to new_member_session_path #does not work
I also tried using now as a last ditch effort:
flash.now[:notice] = "Test flash message"
No dice. Lastly, I have tried placing the flash message inline with the redirect:
redirect_to new_member_session_path, flash: { :notice => "Test flash message" } #also does not work
I am at a loss. Not sure why the flash messages are being lost. I am running Rails 4.1.2 and Devise 3.2.4
EDIT:
I have decided this is not the best way to customize authentication. Instead, I've created a custom Warden strategy and told devise about it:
#config/initializers/devise.rb
config.warden do |manager|
manager.intercept_401 = false
manager.default_strategies(:scope => :member).unshift :member_login
end
I put all my logic in that file:
Warden::Strategies.add(:member_login) do
def member_params
ActionController::Parameters.new(#response).permit(:first_name, :last_name, :role, :username, :parkplace_user_id)
end
def valid?
params[:member] && ( params[:member][:username] || params[:member][:password] )
end
def authenticate!
member = Member.find_by_username(params[:member][:username])
if authenticate_with_ppd_api
if member
member.update(member_params)
success!(member)
else
member = Member.create(member_params)
success!(member)
end
else
fail!("Incorrect username or password")
end
end
def authenticate_with_api
#response = HTTParty.post('https://path.to.server/admin/login', body: { username: params[:member][:username], password: params[:member][:password] } )
#response = JSON.parse(#response.body).first
#response["authenticated"]
end
end
Flash messages are appearing now, but I have other problems with sign in. I created a new question for that. Thanks!
Related
working on a Passwords_Controller that allows the password to be changed, also validates if the user is using same password, or if they are trying to use different password, and another password to confirm. I have it set to "redirect_to" to my dashboard controller that lands them back to be the page to enter the password again before user is updated with new password.
currently I am getting a DoubleRenderError. I have some "puts" with extrapolation see if the logic is passing through the controller and rendering I'm new to rails, so i'm not exactly sure if its my redirect statements that are stopping the validation of old pw, new password - confirm password logic in my controller, or I have too many if statements that is forcing the controller to do double render.
(output of on the terminal with statements for error)
Redirected to http://127.0.0.1:3000/dashboard
true
is this working asdf <~~ test username
is this also working $2a$12$d5WadQunMyww2r4lnmqgveoXaq6WO6hNXvsG/h3RxqUxGFCp6tnWm
Redirected to
Completed 500 Internal Server Error in 738ms (ActiveRecord: 9.1ms | Allocations: 6474)
AbstractController::DoubleRenderError (Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".):
this is my password controller
require 'bcrypt'
class PasswordsController < ApplicationController
include BCrypt
def passwordchanged
end
def update
user = current_user;
#user entered correct password
if user.authenticate(params[:password])
helpers.flash_message :danger, "current password doesnt match"
redirect_to '/dashboard'
end
puts "#{user.authenticate(params[:password])}"
puts "is this working #{params[:new_password]}"
puts "is this also working #{Password.new(user.password)}"
#if new password is the same as new password, doesnt not save
if Password.new(user.password) == params[:new_password]
helpers.flash_message :danger, "new password can't match current password, please try again"
redirect_to '/login'
end
puts "#{redirect_to '/login'}"
#if new password doesnt match confirm password
if params[:new_password] != params[:new_password_confirm]
helpers.flash_message :danger, "password, password confirm needs to match"
redirect_to '/dashboard'
end
puts "#{params[:new_password] != params[:new_password_confirm]}"
#create a new password and save the user ID
user.password = params[:new_password]
user.save
helpers.flash_message :success, "change successful, logout for new session"
redirect_to '/passwordchangesuccess'
end
end
this is my Application Controller
class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
User.find_by(id: session[:user_id])
end
def authenticate_user!
redirect_to '/login' unless current_user
end
end
I'm trying to figure out how I can check if a user reset token is valid BEFORE loading the reset password form. The issue is, currently users don't find out until after they submit.
Here is what I have
class PasswordsController < Devise::PasswordsController
before_action :check_valid_token
private
def check_valid_token
resetCode = (params['resetCode'])
reset_password_token = Devise.token_generator.digest(self, :reset_password_by_token, resetCode)
user = User.find_by(reset_password_token: #reset_password_token)
if user == nil
redirect_to root_path
end
end
end
This doesn't work and I can't find much documentation.
Devise reset password token will be stored as hashed value. You need to decode it.
def check_valid_token
token = Devise.token_generator.digest(User, :reset_password_token, params['reset_password_token'])
user = User.find_by(reset_password_token: token)
user.present?
end
This method will return, true or false
I would do something basic, like this:
def check_valid_token
#user = User.find_by!(reset_password_token: params[:token])
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
so you will have #user instance if token fits and if not it will redirect user to the root_path. You can also add some message before redirecting, like
flash.now[:error] = "Some message here"
I am using has_secure_password with a rails 4.1.5 app. I wanted to decouple my login functionality from my SessionsController so I can reuse it to login any user from wherever I want in my app - for example logging in a user after registration, logging analytics events etc.
So I refactored my code into a LoginUser service object and I am happy with it.
The problem is that my controller still has some coupled logic after this refactoring. I am using a Form Object (via the reform gem) for form validation and then passing on the user, session and password to the LoginUser service.
Here is what the create method in my SessionsController looks like:
def create
login_form = Forms::LoginForm.new(User.new)
if login_form.validate(params[:user]) # validate the form
begin #find the user
user = User.find_by!(email: params[:user][:email])
rescue ActiveRecord::RecordNotFound => e
flash.now.alert = 'invalid user credentials'
render :new and return
end
else
flash.now.alert = login_form.errors.full_messages
render :new and return
end
user && login_service = LoginUser.new(user, session, params[:user][:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
Everything is working as expected but the part I am not happy with is the tied up logic between validating the form and then finding the user before sending it to the service object. Also the multiple flash alerts feel..well..not right.
How would I make this method better by decoupling these two? It seems right now that one is carrying the other on it's back.
For your reference here is my LoginUser service object
class LoginUser
include Wisper::Publisher
attr_reader :user, :password
attr_accessor :session
def initialize(user, session, password)
#user = user
#session = session
#password = password
end
def execute
if user.authenticate(password)
session[:user_id] = user.id
publish(:user_authenticated, user)
else
publish(:user_login_failed)
end
end
end
What sticks out to me the most here is that create is a method with multiple responsibilities that can/should be isolated.
The responsibilities I see are:
validate the form
find the user
return validation error messages
return unknown user error messages
create LoginService object, setup after-auth behavior and do auth
The design goal to clean this up would be to write methods with a single responsibility and to have dependencies injected where possible.
Ignoring the UserService object, my first shot at a refactor might look like this:
def create
validate_form(user_params); return if performed?
user = find_user_for_authentication(user_params); return if performed?
login_service = LoginUser.new(user, session, user_params[:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
private
def user_params
params[:user]
end
def validate_form(attrs)
login_form = Forms::LoginForm.new(User.new)
unless login_form.validate(attrs)
flash.now.alert = login_form.errors.full_messages
render :new
end
end
def find_user_for_authentication(attrs)
if (user = User.find_by_email(attrs[:email]))
user
else
flash.now.alert = 'invalid user credentials'
render :new
end
end
Of note, the return if performed? conditions will check if a render or redirect_to method has been called. If so, return is called and the create action is finished early to prevent double render/redirect errors.
I think this is a big improvement simply because the responsibilities have been divvied up into a few different methods. And these methods have their dependencies injected, for the most part, so that they can continue to evolve freely in the future as well.
I am trying to get up a simple authentication system with Rails' Restful-Authentication plugin, and am just wondering how it works, b/c I can't seem to figure out what the requirements are for cookies, and how to make it so the browser always remembers you (for 6+ months).
Few questions:
1) How do you do remember_me's for ruby's restful_authentication? I can't seem to find a good one-liner to solve this problem...
If a user signs up and checks "Remember Me", how does the rails application get the session/cookie without the user doing anything but going to the page the next time they go to the page, say 3 months later?
2) Do I need to send some sort of info to the server, like their IP address or something? What is cookies[:auth_token], where is that defined?
The goal is: I don't want them to have to enter their email/password again, like how StackOverflow works :)
Here's what we're doing (largely taken from authenticated system) ... this is the controller method that handles login that we're running...
def login
if logged_in?
flash[:notice] = "You are already logged in."
redirect_to "/" and return
end
unless request.post?
render :layout => 'task' and return
end
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
if params[:remember_me].to_i == 1
self.current_user.remember_me
cookies[:auth_token] = {:domain => "#{DOMAIN}", :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
else
self.current_user.forget_me
cookies.delete(:auth_token, :domain => "#{DOMAIN}")
cookies[:auth_token] = nil
end
current_user.last_seen_at = Time.now
current_user.save
session[:notice] = "You logged in successfully"
flash[:notice] = "You logged in successfully"
redirect_back_or_default(:controller => 'dashboard') and return
#redirect_back_or_default(:controller => 'index', :action => 'index') and return
else
if $failed_login_counter.add_attempt(params[:login]) > MAXIMUM_LOGIN_ATTEMPTS
logger.info("login rate limiter kicking in, #{MAXIMUM_LOGIN_ATTEMPTS} login attempts failed")
redirect_to "/denied.html" and return
end
flash[:error] = "Unable to authenticate username and password"
render(:layout => 'task') and return
end
end
And use this for logout
def logout
current_user.last_seen_at = Time.now
current_user.save
self.current_user.forget_me if logged_in?
cookies.delete(:auth_token, :domain => "#{DOMAIN}")
reset_session
flash[:notice] = "You have been logged out."
#redirect_to :back
redirect_back_or_default(:controller => 'index', :action => 'index') and return
end
Then - in your application.rb you'll need something like:
before_filter :login_from_cookie
def login_from_cookie
return unless cookies[:auth_token] && !logged_in?
user = User.find_by_remember_token(cookies[:auth_token])
if user && user.remember_token?
user.remember_me
self.current_user = user
cookies[:auth_token] = { :domain => "#{DOMAIN}", :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
flash[:notice] = "#{self.current_user.login}, you have logged in successfully"
end
end
And - in your User model have some methods like this:
# Encrypts some data with the salt.
def self.encrypt(password, salt)
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
end
# Encrypts the password with the user salt
def encrypt(password)
self.class.encrypt(password, salt)
end
def remember_token?
remember_token_expires_at && Time.now.utc < remember_token_expires_at
end
# These create and unset the fields required for remembering users between browser closes
def remember_me
self.remember_token_expires_at = 2.weeks.from_now.utc
self.remember_token = encrypt("#{email}--#{remember_token_expires_at}")
save(false)
end
def forget_me
self.remember_token_expires_at = nil
self.remember_token = nil
save(false)
end
I'm honestly not sure aboout that particular implementation. But a common RESTful method of authentication is to pass a hashed version of the user/password with each request as a header. Alternatively you can use a hashed cookie value as a header.
I've also seen hybrid systems that involve both. You pass in the session, if you know it, in addition to the user/pass. Then server side if the session is valid it uses that and can cache the session - > user relationship for performance. If the session is invalid, it attempts to authenticate using the user/pass.
In this type of system you'd pass the session back on the response as a header.
Of course that's just a quick rundown of how a system might work, not how ruby's library does.
You can find here a whole tutorial about restful authentication.
http://railsforum.com/viewtopic.php?id=14216&p=13
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.