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.