I would never recommend a client to have login and registration in “pop-up” dialogs instead of plain pages, but sometimes clients choose to kindly ignore my well meant advise and ask me implement such things anyway. I’ve just finished version 1 of an upcoming web application implemented with Rails 3 and – of course – Devise for authentication, and this app just happens to have sign in and sign up taking place in dialogs (or overlays) and submitted to the server via AJAX. I expected this use of Devise to be pretty common use case, but the implementation turned out be pretty tricky.
For this particular web application, the scenario was even more complicated. When an unauthenticated user wanted to comment on something, the sign in box would have to pop up in a dialog. If the user wasn’t registered with the site, he could click a link to get the sign up shown – still in the same dialog. Whether the user registered or just signed in, the dialog should finally display the form for writing a new comment. All this would have to take place through AJAX without the actual page location ever changing, and here is how I implemented it.
Devise relies on Warden for the actual bread-and-butter authentication, and Warden operates purely at the Rack level without knowing anything about your Rails app as such. When Warden encounters a user that is not signed in on a page where he was supposed to be, it will invoke a failure app: A small rack application that decides what to do with unauthenticated users. A core part of Devise is the Devise::FailureApp, which generally stores where the user wanted to go and then redirects to the sign in page. Not so for an AJAX request – or or believe the more correct term is XHR request. In this case, Devise returns a HTTP basic authentication pop-up – yikes! This even happens when http authentication has been disabled by putting
config.http_authenticatable = false in
config/initializers/devise.rb. Usually, we don’t want our users to get this ugly basic authentication box, so first step is to build a custom failure app that ensures that http authentication never happens through Devise:
We configure Warden to use this failure app in the devise initializer:
The observant reader will have noticed that the
redirect_url method is also overridden in
sign_in_and_redirect method of the Devise’s
RegistrationController. We do this by creating our own set of controllers in
app/controllers/registrations_controller.rb. Our new
SessionsController looks something like this:
I’ve got to admit that this is not exactly the prettiest Ruby code I have written, but I’ll leave it as an exercise to the reader to write a beautiful regular expression that can replace lines 10-12. The first couple of lines are unfortunately duplication of the original method, but the information is needed further down. If the action is invoked with an XHR request, the method takes any redirect url (the page the user originally wanted to go to) stored by Devise and makes sure that it’s called with the “js” format. Further it adds an
after_authentication=true query string which was needed in this case, because the redirect was going to the
CommentsController, which needed to know that it should close the sign in dialog before proceeding with displaying the comment dialog.
If there is no stored url to redirect to, we simply render the
after_authentication partial, which in my case closes the sign in dialog and updates the page to indicate that the user has been logged in. If the action is invoked as a standard HTML call, the method simply redirects to
super, so that we can still sign in the old fashioned way. Since I needed exactly the same code in my
RegistrationsController, I put the method in a module and included that in both of the controllers. You might want to do that too.
The final missing piece is to configure Devise to use our new custom controllers rather than the built-in ones. We do that in
routes.rb by means of Devise’s controller options:
Even though I’m not a big supporter of AJAX sign up and sign up – or of much use AJAX anywhere where a standard page refresh makes just as much sense – I must admit that the result was a pretty nice user experience in this case. I hope that Devise gains better support for rendering a response rather than redirecting. Until then, you can use this method.