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 strategy that is visible in application code
- A protocol which can span application frameworks
- Flexibility to transfer without cookies
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.