Handle devise 401 gracefully during sign in - ruby-on-rails

I have a very simple sign_in page for a devise user. Upon submitting incorrect data, the log shows a '401 Unauthorized' and redirects me back to the sign_in page. I couldn't figure out a way to show error messages to the user.
I looked at devise::sessions_controller#create which is as follows,
# POST /resource/sign_in
def create
resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
def auth_options
{ :scope => resource_name, :recall => "#{controller_path}#new" }
end
The flow gets interrupted at warden.authenticate in case of a failed authentication and the user get redirected to the 'new' which is the sign_in page.
I just need to show the user a invalid_credentials tooltip/flash_message. So I did it by modifying :recall => "#{controller_path}#handle_create_fail" (look at auth_options) which calls handle_create_fails when authentication fails, inside which I setup the error messages.
I am not sure if I overlooked something that devise already provides.
How can I handle this better?

'devise' stores error messages in rails 'flash', using flash[:notice] for success messages and flash[:alert] for problems.
Here's this from the devise documentation:
Remember that Devise uses flash messages to let users know if sign in
was successful or failed. Devise expects your application to call
"flash[:notice]" and "flash[:alert]" as appropriate.
This means that in your view file (or more generally in your application layout file) you should include something similar to these lines:
<%= content_tag(:div, flash[:error], :id => "flash_error") if flash[:error] %>
<%= content_tag(:div, flash[:notice], :id => "flash_notice") if flash[:notice] %>
<%= content_tag(:div, flash[:alert], :id => "flash_alert") if flash[:alert] %>
Here are some similar questions/answers:
Rails - Devise - Error messages when signing in?
Devise errors are displayed twice
devise - Customizing the User Edit Pages

I am not sure if I am understanding you correctly, but by simply placing 'alert' in your view (sign_in.html.erb), Devise will flash the error message.
<div class="alert">
<%= alert %>
</div>

I wanted to be able to do something similar, along the lines of form-specific error messaging like you might have on most Rails forms (as opposed to a layout-wide, global flash).
You can check out what I came up with in this Github gist: https://gist.github.com/3792471
Basically (notes below assume a devise scope of :user):
Add/use a custom FailureApp, used by devise+warden, which simply adds a "scoped" flash "indicator". That is, if devise+warden is exiting quickly due to a failed login and has, therefore, set the flash.now[:alert], add flash.now[:user] = [:alert]
Extend FormBuilder (actually, SimpleForm::FormBuilder in my gist, but should work with others) to add a #flash method, which checks for the scoped indicator(s). If found, the matches are extracted/deleted from the FlashHash and rendered. If not found (that is, flash[:alert] may or may not exist, but flash[:user] either does not exist or does not include :alert), do nothing so the flash remains available for the default use case.
Add rendering to relevant views/layouts.
Note that the layout in my example gist does not have conditional logic around the layout-rendered flash messages. I did that just do I could test to see what does and does not show up under various circumstances.

Related

Devise and handling the flash

I am using Devise 3.1.1 with rails 3 and I have this flash handling code in my layout:
<% flash.each do |name, msg| %>
<%= content_tag :section, msg, :id => "flash_#{name}", :class => "flash" %>
<% end %>
I sign into my app, flash says:
"Signed in successfully."
then sign out, then sign in incorrectly and flash says:
"Signed out successfully."
"Invalid email or password."
I think I understand why I am getting two messages, when signing in incorrectly there is no redirect, just a render.
Not sure how to fix it though.
I figured out the reason.
When you dig into Devise's source of SessionsController, you'll find #create method as follows:
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
In above code, Devise sets flash message for success signed in here. That's what the message you saw as "Signed in successfully.". It uses the method set_flash_message which is just a wrapper of flash[key]= "something". The same is true for #destroy method which show you "Signed out successfully".
Note in above code, there is no code to set error message such as "Invalid password or email". So how comes this message you saw? It is set in Devise::FailureApp
def recall
env["PATH_INFO"] = attempted_path
flash.now[:alert] = i18n_message(:invalid)
self.response = recall_app(warden_options[:recall]).call(env)
end
Note here, the method is flash.now, not flash. The difference is flash.now will deliver flash message in current request, not next.
By default, adding values to the flash will make them available to the next request, but sometimes you may want to access those values in the same request. For example, if the create action fails to save a resource and you render the new template directly, that's not going to result in a new request, but you may still want to display a message using the flash. To do this, you can use flash.now in the same way you use the normal flash. http://guides.rubyonrails.org/action_controller_overview.html#the-flash
So the reason is revealed now.
You signed out. You hit SessionsController#destroy. Devise destroyed your session, brings you to /users/sign_in, render 'new template for your sign in again. The flash object contains the successful signed out message and you saw it.
Now you tried to sign in in same page. This time your form submit hit #create. If error, Devise will not redirect you to anywhere but render the same 'new' template again with the flash.now object which contains sign in error message.
In step 2, you last flash object is not removed because no new request rendered, but another new flash.now object is added. So you see two message.
Solution
Of course it's possible to override Devise to change this behaviour, but that's troublesome and unnecessary.
A more convenient and user-friendly solution is, do not land the user on sign in page after either signed in or signed out.
This is easily by setting store_location and override after_sign_in_path_for and after_signed_out_path_for in your application controller.
def store_location
disable_pattern = /\/users/
session[:previous_url] = request.fullpath unless request.fullpath =~ disable_pattern
end
def after_sign_in_path_for(resource)
session[:previous_url] || root_path
end
def after_sign_out_path_for(resource)
after_sign_in_path_for(resource)
end
By this setting the user will land on his previously browsed page either after signed in or signed out, and they will not see two flash messages in the question again.
The reason is, when the user signed out, he will be redirect to previous page and see the signed out message. When he want to sign in, he need to go to sign in page which is a new request, then the previous signed out flash will be removed.

flash notice is lost on redirect, how to find out what's removing it?

There are many posts on SO about this ( respond_with redirect with notice flash message not working Why is :notice not showing after redirect in Rails 3, among others) , I've read at least 4 and still can't solve this issue.
I've got a portion of my site that lets people do some things before they create an account. I prefer this from a UX perspective. So they're allowed to do X and Y then they get redirected to the "Create account" page (uses Devise).
The redirect looks like:
if userIsNew
... stow information in a cookie to be retrieved later ...
redirect_to "/flash", flash[:notice]
=> "Ok, we'll get right on that after you sign up (we need your email)."
and return # this has to be here, since I'm terminating the action early
end
So "/flash" is a plain page that I made to test this. It doesn't do anything, has no markup of its own, just has the basic html from the application.html, which has this line in the body:
<% if flash[:notice] %>
<p><%= notice %></p>
<% else %>
No notice!
<% end %>
It says 'No notice' every time.
I have tried:
adding in a flash.keep to my before_filter in the static controller
using :notice => instead of flash[:notice] =>
putting the notice in a cookie and pulling that text out of the cookie and into a flash in the before_filter of my application controller
redirect_to :back with the flash[:notice] =>
It's either
flash[:notice] = 'blablabla'
redirect_to foo_url
or
redirect_to foo_url, notice: 'blablabla'
I'm overriding ApplicationController#redirect_to to call flash.keep so that any messages are persisted on redirect without having to explicitly call flash.keep in my controller actions. Works well so far. Haven't had a scenario yet where unwanted messages are persisted.
class ApplicationController < ActionController::Base
def redirect_to(*args)
flash.keep
super
end
end
Let me know if there are any scenarios where this isn't a good solution.
I have been fighting with the same problem for some time and none of the posts seemed to help.
It turns out that - like usually it happens - the the problem was in my code. I did have a "redirect_to" that I forgot about, which was clearing the flash.
Namely, "root_path" for me was served by the StaticPagesController's home method. "home" was doing some checks and then redirecting you to the user_path.
In my code I had in numerous places
redirect_to root_path, :flash => {error: #error}
These redirects were never displaying the flash because my hidden "home" controller serving the "root_path" was making another redirect that cleared the flash.
Therefore my problem was solved when i added the "flash.keep" in my "home" controller method
def home
if current_user
#user = current_user
flash.keep
redirect_to #user unless #user.no_role?
end
end
Faced the same problem, flash just disappeared after any redirect, nothing helped, then, I found that it was switched off...
Check your /config/application.rb for this:
config.middleware.delete ActionDispatch::Flash

How to stop soft deleted user's login with Devise

I currently use Devise for user registration/authentication in a Rails project.
When a user wants to cancel their account, the user object is soft deleted in a way like the following.
How to "soft delete" user with Devise
My implmenetation has a small difference this way.
User model has an attribute 'deleted_flag'.
And, soft_delete method executes "update_attribtue(:deleted_flag, true)"
But, I have to implment sign_in action.
In my implmenetation is the following.
class SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
if resource.deleted_flag
p "deleted account : " + resource.deleted_flag.to_s
sign_out(resource)
render :controller => :users, :action => :index
else
if is_navigational_format?
if resource.sign_in_count == 1
set_flash_message(:notice, :signed_in_first_time)
else
set_flash_message(:notice, :signed_in)
end
end
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
end
I think this code has strange points.
If deleted user tries to sing in,
the system permit logging and make log out immediately.
And, the system cann't display flash[:alert] message...
I want to know two points.
How do I implement to prohibit deleted users to login?
How do I implement to display flash[:alert] when deleted user tries to login?
To stop a user that has been 'soft deleted', the best way is to overwrite the find_for_authentication class method on the user model. Such as:
Class User < ActiveRecord::Base
def self.find_for_authentication(conditions)
super(conditions.merge(:deleted_flag => false))
end
This will generate a invalid email or password flash message by devise (because it cannot find the user to authenticate)
As far as your second question though, you'll need some for of method in your controller to add a particular flash message. However, in my opinion you should treat users that are 'soft' deleted the same as if they didn't exist in the database at all. Thus if they tried to log in, they should just get an valid email or password message.
See my solution here: https://stackoverflow.com/a/24365051/556388
Basically you need to override the active_for_authentication? method on the devise model (User).
I haven't tried anything like that but it seems if you want to catch the user before authentication you'll either have to write a Devise authentication strategy or a before_filter to be run before authenticate_user!. Something like:
before_filter :no_deleted_users
def no_deleted_users
if User.find(params[:email]).deleted?
redirect_to root_path, :flash => { :error => "Your user was deleted. You cannot log in." }
end
end
Although it might be more complex to get the user than that. I haven't played with Devise pre-authentication.
The modern and correct answer is this:
class User < ApplicationRecord
def active_for_authentication?
super && !discarded? # or whatever...
end
end
See the documentation here.

Flash not surviving a redirect in Facebook on a Rails application

I have the following two action methods:
def index
puts "==index== flash: #{flash.inspect}"
end
def create
flash[:notice] = "Blah"
puts "==create== flash: #{flash.inspect}"
redirect_to(:action => :index)
end
index.fbml.erb contains this:
<%= button_to_with_facebooker "Blah!", :action => :create %>
The application is used through Facebook. I click the button and the flash contains the notice while create is being executed, but after that it's empty again. It doesn't survive a redirect. Any ideas what's going on here?
I've found one workaround. While using ActiveRecord to store the session, add
ActionController::Dispatcher.middleware.delete Rack::FacebookSession
ActionController::Dispatcher.middleware.insert_before(
ActionController::Base.session_store,
Rack::FacebookSession,
ActionController::Base.session_options[:key])
in an initialization file, like config/initializers/session_store_fix_facebooker_session_key.rb
This has been done on by someone else before and he explained it on a message on the Facebooker group on but it doesn't work with the cookie session storage, Rail's default.

Preserving form submission through log in / sign up in Rails

Say I have a site like this (generic Q&A site) in Rails and I wanted this "ask" page w/ a text box to be the first page a user sees, even if he's not logged in. He enters a question, and on the 'new' method I check that he's not logged in, and bounced him to /session/new, where he can either log in or create a new account. Question is, how do I (and what is the best way to) preserve that question that he initially asked all through this process?
I'm understanding the flow of action described in the question to be
user is presented with a form
user is redirected to log in page on submit
user is redirected back to form on successful log in
repopulate form on load (Question asks how to do this step)
user finally submits their form.
With steps 2-4 omitted if the user is logged in.
I'm sorry, but I see your question more as a symptom of an underlying UI issue than a rails question.
If only logged in users can post questions, then why display the text box?
If a user is going to have log in any way, why not get that out of the way first. An even better solution is to integrate the log in and form.
Something like this in the view:
<% form_for :question do |form| %>
<% unless logged_in? %>
<% fields_for :session do |session_form|%>
<%= session_form.label :login %>
<%= session_form.text_field :login %>
<%= session_form.label :password %>
<%= session_form.password_field :password %>
<%end%>
<%end%>
<%= form.text_area :question %>
<%end%>
And in the controller
def new
...
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
end
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
if logged_in? && #question.save
.... process successful entry
else
... process unsuccessful entry
end
end
Edit: Mohamad's raises the question of reusing this pattern across multiple controllers and forms. So the answer was updated to address reuse of this pattern.
To simplify this for reuse, you could put this block in a helper function that is referenced in the before_filter for actions that require it.
def login
unless params[:session].nil?
self.current_user = User.authenticate(params[:session][:login], params[:session][:password])
if logged_in?
flash[:notice] = "Logged in successfully"
else
flash[:error] = "Incorrect username and or password."
end
end
end
as in:
before_filter :login => :only [:new , :edit, :update, :delete]
On the view side, it shouldn't be too hard to construct a new variant of form_for that embeds the session parameters. Maybe form_for_with_session?
As for handling an unsuccessful response, I would suggest helper function that takes a block of code. Sorry I don't have time to write out or test one for you.
You keep it in the session. So after logging in, when the user goes back to asking his question, you see there's already something in session.
And you can directly display it.
def create
if current_user # Implement this method in your auth framework
#question = Question.new(params[:question] || session.delete[:question])
# (the usual stuff you'd do to save)
else
session[:question] = params[:question]
redirect_to :controller => :sessions, :action => "new"
end
end
Then, after your user creation and authentication stuff is all done in your login action, just make sure you POST back to this create action if session[:question] is defined.

Resources