Hello, I'm Alan

Rails + JWT

Thursday, February 19 2015

I’ve been experimenting with JWT for some client side apps (mainly using React), and I am now in the process of setting up centralised accounts at work.

The important part is only one app is authoriative for a user, and then there are multiple apps which serve API requests for their respective app’s clients.

JWT allows a user to be cryptographically verified without communication required between an API server and our user store. Most of the time it appears JWT is used with a single secret using SHA-256 HMAC, which for a single server works quite well. (When using a shared secret jwt.io is a handy resource for verifying everything is OK)

In our case however a shared secret would mean that any of our apps can generate a valid JWT, which leaves us a larger security issue.

It’s not quite clear how to do it, but the ruby jwt gem supports using RSA public/private key pairs, so now we can verify that the JWT in question came from the correct source but not extend our trust to more services. Also because we store the keys in environment variables, I have base64 encoded the certs so we don’t have escaping issues

private_key = OpenSSL::PKey::RSA.new(Base64.decode64(ENV['JWT_PRIVATE_KEY'])
JWT.encode({ sub: id.to_s, exp: 1.day.from_now.to_i }, private_key), 'RS256')

A JWT can be any JSON object, but the fields sub and exp are reserved. The sub field if used has to be a string to identify the user, with the exp is the date the JWT expires (in seconds since epoch format)

Only the client side we can use the public key to verify that our private key signed this JWT

jwt_public_key = OpenSSL::PKey::RSA.new(Base64.decode64(ENV['JWT_PUBLIC_KEY']))
payload, header = JWT.decode(jwt_string, jwt_public_key)

Lastly the spec says that a client should be able to send an Authorization header of ‘Bearer ’ to authenticate, I have a before_filter in Rails to authenticate these requests

def login_required
  authorization = request.headers['HTTP_AUTHORIZATION']
  if authorization.blank?
    render status: :forbidden, text: 'No authorization header set'
    return
  end

  jwt_string = authorization.split(' ')[1]

  payload, header = JWT.decode(jwt_string, jwt_public_key)

  @current_user = User.find_by!(user_store_id: jwt['sub'])
rescue JWT::DecodeError
  render status: :forbidden, text: 'Invalid header set'
end