JavaScript Web Applications and JSON Web Token (JWT) Security

Background

If you do not want to build and pay for infrastructure to cache Session State so it can be shared across multiple Web servers, then security between the browser and Web Application back-end can be implemented in a stateless manner with JSON Web Tokens (JWT).

Ideally, a JWT is only ever briefly stored in browser memory in a private JavaScript field. In an alternative scenario where a JavaScript Web Application does not have a back-end and is performing API calls to various services or an API Gateway, storing the JWT in memory may be fine, possibly due to that API requiring a one-time Access Token that must be retrieved just before each API call.

However for a Web Application with a back-end, where the front-end only communicates to its back-end and does not perform API calls to other services, if the JWT is stored in memory, a simple browser page refresh will wipe the JWT from memory and the user will be forced to sign-in to the front-end again.

In order to store the JWT Access Token so that it persists between page refreshes, good security practices need to be followed in order to avoid and mitigate the possible attack vectors.

Methodology

The safest methodology is suggested by OWASP - Token Storage on Client Side - see my explanation below.

1. Back-end: Sign-in

On the back-end, after a user’s credentials have been validated successfully:

  1. Generate a cryptographically strong random string - we will name this the fingerprint
  2. Perform a SHA256 hash of the fingerprint
  3. Create the JWT Access Token with the desired expiration time, and a Claim for the fingerprint, with the value of the previously calculated hash.
  4. Add a hardened cookie to the HTTP Response for the fingerprint, with a value of the full random string.
  5. Return the JWT in the HTTP Response Header or Body, depending on how you prefer. DO NOT return the JWT in a Cookie!

NOTE: a hardened cookie specifies:

  • Secure - so that the cookie is only ever transmitted via HTTPS - to help prevent man-in-the-middle attacks;
  • HttpOnly - so that the cookie cannot be retrieved via JavaScript - to help mitigate cross-site scripting (XSS) attacks;
  • SameSite=strict - so that the cookie is sent only ever to the same site as the one that originated it; and
  • '__Host-’ as the prefix to the Cookie name without a Domain attribute and specifying a Path attribute of / - so that the back-end server can confirm that the cookie was originally set on a secure origin (not from a browser) and help mitigate a session fixation attack.

2. Browser: Token Storage

The Web browser will receive both the JWT Access Token and fingerprint hardened cookie.

There is nothing for us to do with the fingerprint hardened cookie. Since this cookie is hardened, JavaScript cannot be used to retrieve it, thus mitigating XSS attacks on it.

Store the JWT Access Token in the browser Session Storage. This will be cleared when the browser Tab or Window is closed.

Since the JWT is stored in Session Storage, it possibly could be stolen by an XSS attack - however we mitigate this risk since the fingerprint Claim in the JWT allows our back-end to prevent reuse of a stolen JWT by an attacker on their machine.

3. Browser: Perform a Back-end Call

When a call is to made to the back-end:

  1. the browser will automatically send the fingerprint cookie as part of the HTTP Request; and
  2. you need to retrieve the JWT from Session Storage and add it to the HTTP Request header or body, depending on your implementation.

4. Back-end: Authorise Web Request

Now completing the picture, when the back-end receives a HTTP Request over HTTPS:

  1. Validate that the JWT was signed by our system;
  2. Validate that the JWT has not expired;
  3. Perform a SHA256 hash on the value of the fingerprint hardened cookie; and
  4. Assert that the hash we just computed is the same as the value in the fingerprint Claim in the JWT.

Conclusion

With all these measures in place, we can be confident that the HTTP Request legitimately came from our front-end.

NOTE: don’t forget also to add a Content Security Policy to restrict the domains from which your web resources may be retrieved, and apply any other relevant defense in depth practices.