JWT Based “Sessions”

4 minute read

OK, you’re in a situation when you can’t use cookies, specifically you’re in an iframe and Safari will only accept a cookie in an iframe if the browser has already visited the site outside of the iframe. Without cookies, there are no sessions. The simple work around is to force the user to visit the site directly, but that’s often not an option. What’s Plan B? As you may recall, our standards for replacing a session cookie with something else are:

  • Protected it with HTTPS.
  • Make the token data opaque.
  • Guard against Cross Site Scripting attacks.

To make sure we’re protected, in a Rails app, simply turn on SSL with:

config.force_ssl = true

in config/environments/production.rb.

If for some reason you can’t use SSL for the whole app… It’s 2017, you have no excuse.

Opaque data (Optional). We are going to need to send an identifier for the current user to the browser. The simplest thing is to just send the database ID so we can do:

User.find_by id

(We’ll get to how we get id in below.)

However, if the database would leak information like how many users we have, we’ll need to do something else.

We can get opacity by creating a UUID when we create the user. A simple (slightly naive) approach. Add this to your model:

class user < ActiveRecord::Base
  before_create :generate_uuid, unless: :uuid?
private
  def generate_uuid
    self.uuid = SecureRandom.uuid
  end
end

and make sure you add an index to your migration:

add_index :users, :uuid, unique: true

(The naivety is that the is a tiny chance of a uuid collision that should be handled.)

Then there’s protecting the value from Cross Site Scripting (XSS) attacks. The scope of that is beyond this post, read up and find an expert if you need help. However, if you don’t allow any user-generated content into your iframe, you’re off to a good start. We’ll also make our tokens expire, which will reduce the usefulness of stolen tokens.

So, what it our token? A JSON Web Token (JWT). I covered these a while back, the short version is that a JWT is a standard for creating a signed blog of JSON as a string. Because it’s a string, it’s easily passed around. Because it’s signed it can’t be tampered with.

Add:

gem 'jwt'

to your Gemfile and bundle install.

Creating a signed JWT is easy:

exp = Time.now.to_i + 5.minutes
payload = {user_uuid: @user.uuid,
           exp: exp
          }
jwt = JWT.encode payload, Rails.application.secrets.secret_key_base, 'HS256'

Payload is the hash to encode and sign. exp is a special key that sets the expiration time, given in seconds since the Epoch. I’ve set it to five minutes in the future, but you’ll want to think about how long it will be before your user resubmits the JWT and tweak accordingly. Rails.application.secrets.secret_key_base, generated when you start a new rails project is a convenient signing key, ‘HS256’ is the signing algorithm. The resulting JWT is simply a string.

Verifying it when you get it back is simple as well:

begin
  jwt = JWT.decode jwt, Rails.application.secrets.secret_key_base ,true, { algorithm: 'HS256' }
rescue JWT::VerificationError
  # Tampered with, handle it.
rescue JWT::ExpiredSignature
  # Expired JWT, handle it.
end
payload = jwt.first

The decoded JWT is an array, with the first element being the payload and the second the containing some meta data about the JWT. Given that:

@user = User.find_by uuid: payload[:user_uuid]

On the client side, it’s a question of making sure you have the JWT to send with any request. In you view, you might do something like:

<script>
  $(function() {
      initialMyAwesomeApp("<%= @jwt %>");
  });
</script>

Your awesome app would store the JWT and send it back in whatever AJAX calls it needs to make:

$.get('/api/object.json',
   {jwt: jwt, object_id: 'something'},
   function(object) { do_something; }
);

This will work pretty well. But, there is one last shortcoming. Cookies are persistent. If you navigate away and then return to a page, the cookie will be sent and the app will have state. The JWT is not persistent, it’s just part of the page, the user won’t be “logged in” next time they hit your app.

While that sounds bad, it’s usually is a non-issue. Truly, you only want to use this technique when you absolutely have to. That’s going to be when you are in an iframe in someone else’s app. Unless that app is utter crap, there’s going to be some kind of verification that you get when your page loads. For example, Atlassian Connect sends a signed JWT in the Authorization header. That verifies the current user when the page loads and all your JWT needs to do is verify your app’s AJAX calls.

In theory, you could use this form of authentication in a single page app, logging the user in when they hit the page and setting up the JWT. But. Don’t. Just don’t. It’s ugly and completely unnecessary.

This is a good technique when you absolute can’t depend on cookies. (And I’ll say it one more time.) If you can, do!

Updated:

Comments