We're trying to extend Devise (3.1.1) signin/signup methods to handle AJAX requests, but are getting stuck with the confirmable logic. Normally, if a user signs in to Devise before confirming their account, they'll get redirected to the login screen with the flash message: "You have to confirm your account before continuing." We can't figure out where Devise is checking for confirmation and making the decision to redirect.
Here's our extended sessions_controller code. It works fine for successful and failed login attempts:
# CUSTOM (Mix of actual devise controller method and ajax customization from http://natashatherobot.com/devise-rails-sign-in/):
def create
# (NOTE: If user is not confirmed, execution will never get this far...)
respond_to do |format|
format.html do
# Copied from original create method:
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
format.js do
# Derived from Natasha AJAX recipe:
self.resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
sign_in(resource_name, resource)
return render :json => {:success => true, :token => form_authenticity_token() }, content_type: "application/json" # Need to explicitely set content type to JSON, otherwise it gets set as application/javascript and success handler never gets hit.
end
end
end
def failure
return render :json => {:success => false, :errors => ["Login failed."]}
end
The problem is, if a user is unconfirmed, the create
method never gets hit. The redirection happens somewhere before, which means we can't handle it in a JS friendly manner. But looking through the source I can't find any before filter that does a confirmable check. Where is the confirmable check happening and how can we intercept it?
What's happening is that the sign_in
method is breaking you out of the normal flow by throwing a warden error, which will call the failure app.
If you look at the definition of sign_in
in lib/devise/controllers/helpers.rb
, you'll see that in a normal flow where you're signing in a user for the first time, you wind up calling
warden.set_user(resource, options.merge!(:scope => scope)
warden
is a reference to a Warden::Proxy
object, and if you look at what set_user
does (you can see that at warden/lib/warden/proxy.rb:157-182
), you'll see that after serializing the user into the session it runs any after_set_user
callbacks.
Devise defines a bunch of these in lib/devise/hooks/
, and the particular one we're interested is in lib/devise/hooks/activatable.rb
:
Warden::Manager.after_set_user do |record, warden, options|
if record && record.respond_to?(:active_for_authentication?) && !record.active_for_authentication?
scope = options[:scope]
warden.logout(scope)
throw :warden, :scope => scope, :message => record.inactive_message
end
end
As you can see, if the record is not active_for_authentication?
, then we throw
. This is what is happening in your case -- active_for_authentication?
returns false for a confirmable
resource that is not yet confirmed (see lib/devise/models/confirmable.rb:121-127
).
And when we throw :warden
, we end up calling the devise failure_app
. So that's what's happening, and why you're breaking out of the normal control flow for your controller.
(Actually the above is talking about the normal sessions controller flow. I think your js
block is actually redundant -- calling warden.authenticate!
will set the user as well, so I think you're throwing before you even get to sign_in
.)
To answer your second question, one possible way of handling this is to create your own failure app. By default devise sets warden's failure_app
to Devise::Delegator
, which allows you to specify different failure apps for different devise models, but defaults to Devise::FailureApp
if nothing has been configured. You could either customize the existing failure app, replace it with your own failure app by configuring warden, or you could customize the delegator to use the default failure app for html requests and delegate to a different failure app for json.
I met same problem before. Actually it can be solved very simply.
Go to config/initializers/devise.rb
, find the following line
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming his account. For instance, if set to 2.days, the user will be
# able to access the website for two days without confirming his account,
# access will be blocked just in the third day. Default is 0.days, meaning
# the user cannot access the website without confirming his account.
config.allow_unconfirmed_access_for = 0.days
Activate the last line and set the period as what you like, say 7.days
This period is a grace period, once set, you can expect
User can be signed in automatically after signing up, no more redirection to confirmation page.
Within this period, unconfirmed user can be remembered, and signed in without problem.
After the period expired, unconfirmed user will be redirect to confirmation page and must confirm password before continuing.
With :confirmable
enabled, I'm afraid the grace period is the most user friendly. If you are even not satisfied with that, there is little points to use :confirmable
:)
I just noticed you used JSON response when I read Rich Peck's comment. I would suggest you not to use JSON response, instead, a simpler approach to use an Ajax/Plain JS form as login form, and expect Devise to do normal page redirect. The reason is there would be too much resources to update if using JSON response, for example, csrf_token, cookie, existing authorization etc. See my other answer about similar topic: https://stackoverflow.com/a/19538974/1721198
@gregates gets credit for figuring this out, but I wanted to show how I actually got this working:
The problem was you can't check for or intercept an unconfirmed user at the controller level because it gets checked at the model level with active_for_authentication?
, and then gets handled by Warden's failure app immediately. So any reference to current_user
will short-circuit whatever controller logic you've got going on. The solution is to introduce your own custom FailureApp, and handle things as you see fit. In our case, that meant raising a special error for AJAX auth errors. Here's how we did it:
First, create a custom failure app, as described here:
How To: Redirect to a specific page when the user can not be authenticated · plataformatec/devise Wiki
lib/custom_failure.rb:
class CustomFailure < Devise::FailureApp
def respond
# We override Devise's handling to throw our own custom errors on AJAX auth failures,
# because Devise provides no easy way to deal with them:
if request.xhr? && warden_message == :unconfirmed
raise MyCustomErrors::AjaxAuthError.new("Confirmation required. Check your inbox.")
end
# Original code:
if http_auth?
http_auth
elsif warden_options[:recall]
recall
else
redirect
end
end
end
And then tell Warden to use the custom failure app:
config/initializers/devise.rb:
config.warden do |manager|
manager.failure_app = CustomFailure
end
And then you just handle your custom error however you like. In our case, we had our ErrorController return a 401 JSON response.
To add to @gregates answer, try overriding the method active_for_authentication?
in your User(resource) model like this:
def active_for_authentication?
true
end
If you are able to login, then the issue has to be with your sessions controller. Make sure you havent missed anything obvious like specfying the route to your new sessions controller
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With