OVO Tech Blog

Json Web Tokens

Introduction

Steve Fleetwood

Steve Fleetwood


Json Web Tokens

Posted by Steve Fleetwood on .
Featured

Json Web Tokens

Posted by Steve Fleetwood on .

What is a JWT?

JWT stands for JSON Web Token and is defined in RFC7517 as:

   JSON Web Token (JWT) is a compact, URL-safe means of representing
   claims to be transferred between two parties.  The claims in a JWT
   are encoded as a JSON object that is used as the payload of a JSON
   Web Signature (JWS) structure or as the plaintext of a JSON Web
   Encryption (JWE) structure, enabling the claims to be digitally
   signed or integrity protected with a Message Authentication Code
   (MAC) and/or encrypted.

Mostly you will see JWTs used in OAuth 2 frameworks, within which there are different types of tokens; Refresh Tokens, Access Tokens, Identity Tokens and JWT be used can represent them all.

JWT and OAuth are often confused with each other, OAUTH is an authorization framework that enables applications to obtain limited access to user accounts, this access is provided using tokens, these tokens can be JWT, but they do not have to be.

Why use JWTs

The big win for users of JWTs is that you do not have to go back to the issuer, on every request, to validate the contents of a token or to find out more about the subject. Imagine the following scenario:

  • A user (subject) logs into the 'My Product' website
  • The user goes into a page allowing them to create 'Items'
  • They submit a form to create an item
  • A request wings it's way down the 'My Product' application stack to the backend service responsible for creating 'Items'
  • This service needs to know if the user (subject) is allowed to create "Items"

If the JWT (Access/Bearer Token) is included in the request to the service, it could just interrogate the token for a 'backend-service' > 'create-items' role, and if this role is present process the request, otherwise reject it would reject it with an unauthorized response.

This is opposed to approaches where a session token is passed to the service and it has to hit the issuing service with the session token to obtain information on the user (subject).

Obviously the service cannot just trust any old JWT token it receives, it must be verified (verification is covered at the end if you make it that far).

Structure

So what is actually in a JWT?

A JWT is made up of:

  • A Header
    • A JSON object whose items contain information about what the token is and how is signed
  • A Payload
    • A JSON object whose items represent the information contained in the token (the claims)
  • A Signature
    • Created by encoding the Header and Payload, then hashing using a specified hashing algorithm and secret

Let's look at these in a bit more detail

Here is an example header from a JWT Bearer (Access) token generated using a locally running instance of Keycloak:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "n_TdQEebp0Y3TiG9vTUA8QaG73jj-zATdFrto0dimNg"
}

As per RFC7517 (detailed above) the JWT can be signed as a JWS (JSON web Signature), which is what Keycloak does, in this case the header contains information on how the token was signed and what it is.

  • typ - Indicating the type of token, a JWT
  • alg - The signing algorithm used
    • In the example from Keycloak the algorith used is RS256, meaning the server generated a signature using a public/private key pair, which the consumer can validate using the public key
  • kid - A hint as to which key, on the issuing service, was used to generate the signature

Payload

Here is an example payload from a JWT Bearer (Access) token generated using a locally running instance of Keycloak:

{
  "jti": "316fec1f-6d70-406a-bca3-b95ddd9546e3",
  "exp": 1525435773,
  "nbf": 0,
  "iat": 1525435713,
  "iss": "http://localhost:8080/auth/realms/master",
  "aud": "my-product",
  "sub": "2bdf4147-fb94-471b-827f-634ab1899b73",
  "typ": "Bearer",
  "azp": "my-product",
  "auth_time": 0,
  "session_state": "c2229bb7-5e52-483a-80c2-c2a990ed75eb",
  "acr": "1",
  "allowed-origins": [],
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    },
    "backend-service": {
      "roles": [
        "create-items",
        "delete-items"
      ]
    }
  },
  "name": "Steve Fleetwood",
  "preferred_username": "steve.fleetwood",
  "given_name": "Steve",
  "family_name": "Fleetwood",
  "email": "my.email@ovoenergy.com"
}

The JSON object is made up of the claims contained in the token, there are different types of claims within the payload:

Registered Claims

These are essentially reserved claims in OAUTH, but are still optional:

  • jti - Id
    • A Unique identifier for the token (used to prevent the token from being replayed).
  • exp - Expiration Time
    • NumericDate value representing the time after which the token must no longer be accepted.
  • nbf - Not Before
    • NumericDate value representing the time before which the token must not be accepted.
  • iat - Issued At
    • NumericDate value representing the time the token was created
  • iss - Issuer
    • Case sensitive string identifying the principle that issued the token
  • aud - Audience
    • May be a single or array of case sensitive strings identifying who the token is intended for use by
    • If this claim is present but the value is empty the token must not be accepted
  • sub - Subject
    • Identifies who the subject of the token is
      • If the token was generated by a user authenticating themselves, then it will be the unique id of the user (user Id)
      • If a client generated the token authenticating itself then will be the unique id of the client (client Id)

Private claims

These are claims that are agreed between issuer and consumers of the tokens.

In the example from Keycloak you can see various private claims, a couple of interesting ones being:

  • resource_access - Representing roles held by the subject, allowing access to certain resources
    • 'backend-service' > 'create-items' - Some made up role allowing the subject to "create items" in the "backend-service"
    • 'account' > 'manage-account' - A role from Keycloak allowing the subject to manage their own account
  • typ - Type of token
    • Keycloak uses the OAUTH 2 protocol, so could be Bearer, Refresh, ID. Bearer here is an Access Token.

Other pretty self explanatory private claims in the Keycloak example are name, preferred_username, given_name , family_name, email

Public claims

Public claims are again claims that are determined by the issuer, but they must be named in such a way to prevent collisions with other public claims.

What does a JWT look like

The above examples are what the parts of a JWT token look like when they have been parsed, the format of actual token when it is passed around in requests, stored in cookies, etc is explained below.

JWS - JSON Web Signature

When a JWT is issued as a JWS it looks like this:

eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJuX1RkUUVlYnAwWTNUaUc5dlRVQThRYUc3M2pqLXpBVGRGcnRvMGRpbU5nIn0.eyJqdGkiOiJhYmRhMDJhNi1lM2IzLTRhYTQtOTJiYy04ZTY0MDM1YjRkMDMiLCJleHAiOjE1MjU0Mzc1MTMsIm5iZiI6MCwiaWF0IjoxNTI1NDM1NzEzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoibXktcHJvZHVjdCIsInN1YiI6IjJiZGY0MTQ3LWZiOTQtNDcxYi04MjdmLTYzNGFiMTg5OWI3MyIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJteS1wcm9kdWN0IiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiYzIyMjliYjctNWU1Mi00ODNhLTgwYzItYzJhOTkwZWQ3NWViIiwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfSwibXktcHJvZHVjdCI6eyJyb2xlcyI6WyJjcmVhdGUtaXRlbXMiLCJkZWxldGUtaXRlbXMiXX19fQ.kZxN2l55dUI--U-6rXAs3T110hHHylPRKV-a_9V1jwMwPCG-FfYNEfuC4ro9fPb51r5WO2lTetkCSHDh1mJeUIW4kpvYAhWkjfx0IHgupPDQrZyAe7zy8piyJqPk4Mhp6h1qRCUXH68I4qwoWu5IE8XaK1znU6jcvs20Dtu1Dl81xrp9Wqp5Xj658fuNj4r0guPxGnskjmrHPJt_0e_7lzs3wA51qVadUeW7KMusP1IBdMlpuk0LxTK5bz5QoeumwGDym31LsQ617JTE3RTY2x3XtPNHwjoYiQfsatfXGXziayOch8AsoQ37-orDvr_oj_rn3RAydNvmwrI6qcBLxQ

It is massive, but is made up of the header, payload and signature parts, Base64 encoded and separated by a ., basically aaaaaaaaaa.bbbbbbbb.cccccccc

Signature

As in the Keycloak example above, the signature is a hashed value, generated from the specified algorithm and secret, applied to the encoded Header and Payload, it looks like:

✻ Signature bsIJ3Wle9_n5VK0_fWK9VwyxQUR2MpUDjOBIQBc-Rvv71cF2qdDVw9PIFKkFdPPKkNGrsfMjpMal_2IeW9UtUmnfV7b6O0q2GLtk5GV7jsBDZaMG4nO1XhBhU0wsRT3soORxap3HqSDeX93VLOOeAq4XCx0koW6oUpDCEQcu-b8Ns-nin6CiZyI2S_pgP8dvH2jncvBl_OBT2j9BxjPYIQadmPyE4xief4UUyz_PA_Iw48P21tVviOaq-H8_uZSZ3FoWAtD4Ygz8XUalylXe5RntB_jYU4vcz5KCBLvcVJVQX2cqRSP7ulYJ5q3vDgaAgGkG4A7oPulGeHiGNukxWw

Note - As more claims are added to a JWT the bigger the JWS gets, so care should be taken if storing these in cookies, etc where the is a size limit.

JWE - JSON Web Encryption

Whereas JWS tokens are Base64 encoded, meaning the contents can be read by anyone, JWE tokens are encrypted with the intention that they can contain sensitive values in the claims, that only the intended receiver can decrypt and read.

I am going to skip how this encryption in implemented for now, as is a blog post in it's own right, and this post is dry enough as it is :)

Verification

Before trusting the contents of a JWT, some steps to ensure that the token has been issued by the correct party, is still valid and hasn't been modified could be:

  • Ensure the token is well formed
  • Ensure the iss (Issuer) is the one expected
  • Ensure the alg (Algoritm) is the one you are expecting the issuer to have used
  • In the case of RS256 obtain the public key from the issuer (this can be cached)
    • Verify that the signature is correctly formed, using the public key, from the header and payload
  • Ensure the exp (Expiration Time) has not passed
    • Due to clock skew is it prudent to give a few seconds of leeway
  • Ensure the nbf (Not Before) has passed
    • Due to clock skew is it prudent to give a few seconds of leeway
  • Ensure the aud is as expected
  • Check any other private claims

That is a lot of stuff to check, but luckily there are libraries in most languages that can do this for you, see https://jwt.io/ which has a nice list of them.

Steve Fleetwood

Steve Fleetwood

View Comments...