TL;DR
If you want to be able to destroy a Rails session_store = :cookie_store
session from the client, you have to tell Rails not to use an HttpOnly cookie. In Rails 4, add this to your config/initializers/session_store.rb
:
YourApp::Application.config.session_store :cookie_store, key: 'your_key_name', httponly: false
…and then destroy the session cookie in JavaScript, e.g.,
deleteCookie = function(name) { document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; } deleteCookie('your_key_name');
TL;RA:
So we’ve been dealing with this problem for about a year now. Our application’s back-end REST API is written in Ruby on Rails, with Devise as our authentication mechanism, and our user-facing EmberJS (Javascript) apps consume the API.
We have been using the Rails :cookie_store
, which lets you store all of the session data in an encrypted cookie. The good news about this is that you don’t have to make a database request for the session data every time the user makes a call; the bad news is that it’s very hard to log out, because the only way to destroy the session data is by sending an update from the server itself. In fact this is a widely recognized issue; for more information you can see this blog post.
Now, what we really should do is switch to something like a Redis store or the built-in :active_record_store
, just like that blog post recommends. We actually did that in our app, but we experienced problems with users being asked to log in over and over again. Session persistence was an issue, so we had to make a (very Agile) decision to switch back to something that worked. While switching back to a :cookie_store
means they users don’t have to log in again and again, it also means that they can’t log out. And we have extra complexity because we have a special Rails controller that handles launching into multiple apps based on user permissions. It’s going to need to be re-written at some point, but in the meantime we needed a fix.
Here’s the background: Rails uses a session cookie with the HttpOnly flag set. HttpOnly is a security feature that means a client (e.g. with Javascript) cannot directly update the contents of a cookie; updates can only come from the server. So you’d think it would be easy to destroy a cookie in Javascript and get around this whole problem of not being able to log out (because deleting the session cookie definitely logs you out). Except we can’t do that because of HttpOnly. Javascript can simply not write to an HttpOnly cookie. This is generally A Good Thing for security, so it’s hard to criticize Rails for making this choice (though I’m sure that won’t stop people from criticizing anyway). There was some discussion about using expiring cookies, etc., for cookie stores at https://github.com/rails/rails/pull/11168, but this comment from the pull request’s author is very telling:
Giving up. The inheritance and mixins approach in the chained and legacy cookie jar code is absurd beyond my patience.
Someone else is free to run with this code as a start and take the credit.
The proper solution is to use something like the :active_record_store
or a :redis_store
, where your authentication and authorization can just destroy the session in the database or Redis store. Maybe you’re in the situation we are, though, where that isn’t feasible. Well, I has solution.
The good news is that you can tell Rails not to make cookies HttpOnly, and it requires a single change in your initializer. You just need to add httponly: false to the session_store initializer. In Rails 4, add this to your config/initializers/session_store.rb
:
YourApp::Application.config.session_store :cookie_store, key: 'your_key_name', httponly: false
Of course, you’ll have to manually destroy the cookie in the browser, or use something like Devise to sign out all users so they can pick up the new non-HttpOnly key. Once you do that, though, you can use any number of Javascript libraries to fiddle with cookies. There are several good ones, but I just wrote a little helper because all I needed to do was delete a cookie, and I didn’t want to import an entirely new library for a function I could write in one line:
deleteCookie = function(name) { document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; } deleteCookie('your_key_name');
Well, mine is in CoffeeScript:
deleteCookie = (name) -> document.cookie = "#{name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC";
Now, when the user logs out, the Javascript app sends a request to the Rails API to log them out, and once the Ajax callback is complete, I destroy the session cookie. Now when the app tries to redirect them to another page, their session cookie is missing, so Rails no longer knows who they are and sends them to the Devise sign in page. Here’s the CoffeeScript we’re using (I would apologize for it, but I love CoffeeScript) that removes the auth token we keep in localStorage and nukes the cookie:
deleteCookie: (name) -> document.cookie = "#{name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC"; _clearSession: -> @deleteCookie appSessionCookieName localStorage.removeItem('token_name') App.set 'token_name', undefined _redirectToSignin: -> window.location.href = '/users/sign_in' actions: logout: -> Ember.$.ajax('/users/sign_out', { type: 'DELETE' }).then => @_clearSession() @_redirectToSignin()
This was a frustrating problem with a simple solution. Keep in mind that you probably shouldn’t be using cookie stores anyway, but if you do, this is a quick fix for log out.