Eric Radman : a Journal

Practical JWT

JSON Web Tokens (JWT) provide a standardized representation of claims to be transferred between two parties. This payload may contain any JSON data, but for web services this is most commonly applied to stashing some bit of data that identifies a user that has passed a prior authorization step.

Keeping the length of the encoded string short is important to minimizing overhead, especially for cookies which are added to the header in every HTTP request.

What are Sessions?

Many web frameworks provide a feature called sessions which provide a client authorization features such as token expiration and URL-safe encoding based on a secret key.

The big difference is that JWT is a more general approach that allows applications to determine their behavior. Building a token and choosing a mechanism for transferring the token between the client and server brings several benefits:

A Simple Application

The following example uses the simple ruby-jwt

require "sinatra"
require "sinatra/cookies"
require "jwt"

post '/account' do
    # Verify credentials
    payload = {:email => email}
    token = JWT.encode(payload, hmac_secret, 'HS256')
    cookies[:access_token] = token
    redirect to("/account")
end

Where hmac_secret can be a variable set at server startup, or a function that will read a secret from disk

def hmac_secret
    @hmac_secred ||= File.read(".hmac_secret").strip
end

In the next example we will read back this cookie access_token using the same algorithm and secret

def read_access_token
    token = cookies[:access_token]
    return JWT.decode(token, hmac_secret, true, {algorithm: 'HS256'})[0]
end

There are a range of conditions you will also need to handle: Is the value set? Is the value empty? Was there a decode error?

Automated Tests

The scaffolding for running tests depends on the details of your web framework, but the overall design involves first writing (or mocking) a secret that will be common to the tests and to the application

url != pg_tmp

test:
   echo "ZWVMeDWgOmHFU1NwTliW" > .hmac_secret
   DATABASE_URL=${url} ruby31 tests/authorized.rb

The test runner will then this secret to generate a legitimate JWT token

# authorized.rb
def setup
    payload = {:email => "ericshane@eradman.com"}
    @access_token="access_token=" + JWT.encode(payload, hmac_secret, 'HS256')
end

Then test for authorization failure without the token set, followed by a successful request when the token is set

class AuthenticatedTest < Minitest::Test
    include Rack::Test::Methods

    def test_get_install_sitecode_id
        get '/install/5c188e73-bbc8-4c1b-96c2-d4a195bd6cef'
        assert_equal last_response.status, 401

        set_cookie @access_token
        get '/install/5c188e73-bbc8-4c1b-96c2-d4a195bd6cef'
        assert_equal last_response.status, 200
        assert_equal last_response.content_type, "text/html;charset=utf-8"
    end
end

This pattern provides very good test coverage, since authoriziation itself is not stubbed out.