Getting AJAX Pointer in Confirmation

We are trying to extend the Signin / Signup (3.1.1) methods for handling AJAX requests, but they are stuck with validated logic. Typically, if a user signs up for Devise before verifying their account, they will be redirected to the login screen with a flash message: "You must confirm your account before proceeding." We cannot understand where Devise verifies the confirmation and makes the decision to redirect.

Here is our extended session_controller code. It works great for successful and unsuccessful 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 that if the user is not authenticated, the create method never hits. The redirection happens somewhere earlier, which means that we cannot handle it in a friendly JS manner. But, looking through the source code, I can not find a filter before that which will check. Where does the verification check take place and how can we intercept it?

+6
source share
4 answers

What happens is that the sign_in method pulls you out of the normal thread, throwing a boss error that will cause the application to fail.

If you look at the definition of sign_in in lib/devise/controllers/helpers.rb , you will see that in normal mode, when you first sign up for a user, you end the call

warden.set_user(resource, options.merge!(:scope => scope)

warden is a reference to the 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 will see that after the user is serialized in the session, makes any after_set_user .

Devise defines a group of them in lib/devise/hooks/ , and the specific one that interests us 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 entry is not active_for_authentication? then we throw . Is this what happens in your case - active_for_authentication? returns false for a confirmable resource that has not yet been confirmed (see lib/devise/models/confirmable.rb:121-127 ).

And when we throw :warden , we call failure_app . So what happens and why you are breaking the normal control flow for your controller.

(Actually, the above is talking about the normal flow of the controller controller. I think your js block is actually redundant - calling warden.authenticate! also set up the user, so I think you quit before you even get to sign_in .)

To answer the second question, one of the possible ways to solve this problem is to create your own application with an error. By default, devset sets warden failure_app to the Devise::Delegator , which allows you to specify different applications with failures for different development models, but the default is Devise::FailureApp if nothing has been configured. You can either configure an existing application with failures, or replace it with your own application for failure by setting up a supervisor, or you can configure the delegate to use the application with a default error for html requests and delegate another application with an error for json.

+8
source

@gregates gets credit for this, but I wanted to show how I actually got this:

The problem was that you cannot verify or intercept an unacknowledged user at the controller level, because he is verified at the model level with active_for_authentication? , and then immediately processed by the Warden application. Therefore, any reference to current_user will lead to a short circuit of any controller logic. The solution is to submit your own FailureApp and handle things as you see fit. In our case, this meant raising a special error for AJAX auth errors. Here's how we did it:

First, create a custom application for failure, as described here:
How to do: Redirect to a specific page when the user cannot be authenticated Β· plataformatec / devize Wiki

lib / custom_failure.rb:

 class CustomFailure < Devise::FailureApp def respond # We override Devise 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 application for failure:

config / Initializers / devise.rb:

  config.warden do |manager| manager.failure_app = CustomFailure end 

And then you just handle your custom error as you like. In our case, our ErrorController returned a 401 JSON response.

+3
source

I used to encounter the same problem. In fact, this 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 you like, say 7.days

This period is a grace period, after its installation you can expect

  • The user can be signed up automatically after registration, is not redirected to the confirmation page.

  • During this period, an unconfirmed user can be remembered and signed without problems.

  • After this period, an unacknowledged user will be redirected to the confirmation page and must confirm the password before continuing.

If enabled :confirmable , I'm afraid that the grace period is the most convenient for the user. If you are not even satisfied with this, there is little point in using :confirmable :)

JSON reaction side notes

I just noticed that you used the JSON response when I read the Rich Peck comment. I would advise you not to use the JSON response, instead, a simpler approach to using the JS Ajax / Plain form as the login form and expect Devise to do the normal page redirection. The reason is that there will be too many resources for updating if you use a JSON response, for example, csrf_token, cookie, existing authorization, etc. See my other answer on a similar topic: fooobar.com/questions/956120 / ...

+2
source

To add @gregates to the answer, try overriding the active_for_authentication? method active_for_authentication? in your User (resource) model as follows:

 def active_for_authentication? true end 

If you can log in, the problem should be with your session controller. Make sure you don’t miss anything obvious like pointing out a route to a new session controller.

+1
source

Source: https://habr.com/ru/post/957122/


All Articles