It’s not uncommon to have login-required forms accessible to users who aren’t logged in — for example, maybe you’re trying to encourage someone to start writing some content without having to bother logging in first. Of course, they have to log in before they can post, but what happens when they push that big “post” button? Or take another example: you have a button to thumbs-up some content. Only logged-in users should be able to thumbs-up any content, but you always want to display the button. But then what happens when someone who’s not logged in presses the button? In most Rails applications, they’d be logged in, redirected back to the page they were referred from, and they’d have to click the thumbs-up again.
That sort of sucks. They already clicked it once. Why can’t we remember that?
I ran into this problem myself today in the context of the thumbs-up button. After doing some research and realizing there was no great Rails 3 solution to the problem, I decided I would roll one myself. The result is some complicated but awesome Rack middleware that I think would be pretty handy for most Rails developers.
First, put some code wherever you redirect to force login. I do that when I see a
CanCan::AccessDenied error, but you should do it wherever makes sense for you. Here’s an example method very similar to the one I use.
1 2 3 4 5 6 7 8
login_path should redirect somewhere sensible: I chose sessions#new. Once there, we reset the session (to prevent any funny business) and then assign a bunch of attractive new session variables.
1 2 3 4 5 6 7 8
Since I’m using OmniAuth and Facebook, this will result in the user being logged in. In the login callback — I chose sessions#create — we need to do some special detection and redirection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
The interesting portion is
redirect_to '/redirect_back'. That’s our middleware hook: when the session variables are set correctly, and you go to
/redirect_back, the middleware will activate and do a POST to the application. So, in essence,
/redirect_back stands in for whatever request you just tried to go. That’s why this doesn’t work with GET requests: with a GET, the new URL for the page will be
/redirect_back regardless of what the page is, which looks pretty weird.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
The whole idea of this middleware is to reformat the path and add in all the appropriate params from the session variables, and then call the Rails application with it. Rails believes that — despite the URL being
redirect_back — we’ve made a correctly-formatted post and processes it correctly, sending another redirect to the client when it’s completed.
The only problem is that, since we’re dealing with forms here, Rails needs a CSRF token. Without it, it’ll purge the session at the beginning of the request and we’ll end up in a redirect loop.
However, remember how we
reset_session at sessions#new? We can be confident that this session is not fixated; we cleaned it before entering the middleware, and the middleware only deals with session variables. Provided you don’t use the cookie session storage mechanism (and you should not be), you can be sure enough of the user’s identity to ignore the authenticity token on this one request.
We also need to ensure that Rails places a new, correct CSRF token into the session. Otherwise users’ sessions will be deleted by the CSRF handlers upon their next post, which will seem to have an invalid, old token.
1 2 3 4 5 6 7 8 9 10 11 12 13
As a bonus we also clean out the session a little bit so that
/redirect_back won’t work again.
And that’s that! Now a user can initiate a POST and be automatically logged in, see the POST completed, and be redirected to the output — all with just one little button press. Though the Rails 2 method of doing this was substantially easier, I think this middleware method has an elegance to it. And the usability certainly can’t be beat.