Safely Handling JWTs
Tags: security, api, jwt
Published: 2022-02-02 13:58:54


<figure align="center">
  <img
  src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oat95cmbis0kmoccmry8.png"
  alt="MEME with character Mugatu from film Zoolander played by Will Farrel  saying to a colleague, JWTs, so hot right now. He is carrying a small white dog dressed in clothes and a tie matching his own." width=300>

</figure>

## Introduction
This article is focused on helping people already using JWTs today.
I did not want this article to try and persuade you to or not to use JWTs, there are plenty of those already. What I struggled to find was a summary for existing users. So if you are using them, let's help make sure you are handling them safely.

## TLDR
### What they are
* JWTs - JSON Web Tokens pronounced as “JOT”.
* User input so treat them as untrusted.
* Start with `eyJ` (base64d JSON object).
* Have 3 parts: header.payload.signature separated by dots.
* Able to be signed by a variety of algorithms including none (no signature at all).
* Hard to get right! Even some of the best get it wrong: [Auth0's 2020 validation bypass](https://insomniasec.com/blog/auth0-jwt-validation-bypass).
* Come in different types: An example being an `access token`.

### The problems they can solve
* Exist to allow claims to be transferred between two parties
* Could be used to support solutions secured by OAuth2.0 and OpenID, to allow 3rd party applications to call the API on behalf of the user without the 3rd party having the users credentials (depending on the implementation). Meaning the user may not have an active session with the 1st party application but the API can still be called.

### What you need to do
* Choose *one* algorithm and stick to ONLY ONE such as RS256. Turn off the rest including the option for none. 
* Use an identity provider to create JWTs.
* *Decode != verified* you need to do both before acting on the information.
* Don't write your own decode or verify functions, use a library like https://github.com/auth0/node-jsonwebtoken.
* If it doesn't decode, **reject it**
* If it doesn't verify **reject it**
* Verify functions often have a lot of options. Use them!
  * Algorithm
  * Issuer
  * Audience
  * Issued at isn't in the future
  * Not before isn't in the future
  * Bonus: expiry clock tolerance (max 30 seconds)
* Check the scopes
* Check the claims
* Bonus: If you can apply a type to the token. Check it's an access/ bearer token. 
* If you are rotating signing certificates *which you should*, check if you have the public key matching the `kid` in the header before trying to validate.
  * Check the issuer is trusted before attempting to retrieve new certificates. If it's not, **reject it**.
  * Ideally your services would already have the latest / current key through another means after the rotation. 
  * Do not recklessly trust the issuer field and retrieve new public keys from it's JWKS_Uri!
* If you're using a symmetric algorithm like HS256, make sure the secret is changed to something unique and long enough. (At least as long as the resulting token)

## TLDR not enough? 

### What is a JWT?
[RFC7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) defines a JWT (Often pronounced “jot”) 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.
<p>

* They have 3 parts separated by dots (note the colours in the figure below). 
* Each are base64 encoded separately.
* They are instantly recognizable as they start with eyJ… (the first 2 parts are JSON objects when when base64 encoded become eyJ).

You may have seen them in cookies or Authorization headers with the word “bearer” in front of them. 

Below is an example of a access token that is a JWT in encoded and decoded form using https://jwt.io
<p align="center">
<img  src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxzt8a8mop6ew392ydod.png" alt="An example JWT from https://jwt.io" width="500"/>

The 3 sections are as follows:

* The first section shown in red is the `Header` (JOSE Header) which tells the receiving party how it's signed and which key has been used (if appropriate).
* The second section shown in purple is the `Payload` with some standard properties based on the RFC but can also include custom properties/ claims.
* The third section shown in in blue is the `Signature` for consumers to use to validate that the JWT was issued by the trusted authority.

The access token can be likened to a passport that states some claims and is certified by a trusted authority. Like a passport, they can be tampered with so we need to ensure we properly validate and verify the contents using the tools available and protect ourselves from the numerous vulnerabilities that have been discovered and treat all input as untrusted user input.

### Signing algorithms
Your authorization server will let you choose which algorithm you want your tokens signed with.
<p>
Choose one, use one and only one if you can. The verify function will need to be told which you support and will reject anything else. You could even check before you verify and if you receive a token with anything else, reject it.

Algorithm names are usually split into 2 parts:
<p align="center">
<img  src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qm4ovm3m9izsgxw374k7.png" alt="Signing algorithm diagram showing that the first 2 characters relate to the signature algorithm and the remaining characters relate to the hashing algorithm. The example shown is for RS256" width="500"/>

* Signature algorithm: In our example, RS means RSASSA-PKCS1-v1_5 (typically)
* Hashing algorithm In our example, 256 means SHA-256
<p>
Together it's known as “RS256”, a very popular JWT signing algorithm. It has been the default for Auth0 since around 2019.

Note: The latest signing algorithm “EdDSA” doesn't follow the same naming setup as the others (as does the “none” algorithm).

[RFC7518](https://datatracker.ietf.org/doc/html/rfc7518#section-3) defines a list of 13 options (One being “none” where the JWT has no signature at all!) 

<p align="center">
<img  src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kgz4fhnhiky2abi2n60o.png" alt="List of supported JWT signing algorithms taken from RFC7518" width="500"/>

In addition to the RFC, there are others available which you can find on the iana registry: [JSON Object Signing and Encryption (JOSE)](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms)

### What should I use?

**RS256** and **HS256** are the two most popular signing algorithms and will have the best support from libraries you might use. Two others to note are ES256 and PS256 which are mandated by OpenBanking so are gaining popularity. Another new algorithm to take note of is EdDSA which is recommended by cryptographic experts but today lacks the universal library support offered by some of the older options.

**RS256** - Asymmetric (Private Key to sign and Public key to verify) using RSASSA-PKCS1-v1_5 [RFC 3447 - Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1)](https://datatracker.ietf.org/doc/html/rfc3447) with a SHA-256 hashing

**HS256** - Symmetric (shared secret used by signer and verifier) using SHA-256 hashing

Both are as cryptographically safe as each other but RS* type algorithms have the advantage of easier implementation and maintenance as you do not have to do secrets management in your consuming service. This has the added benefit of a stronger trust level as no shared secret is ever exposed. Not exposed = reduces chance of it leaking and tokens from rogue actors being received.

The other possible negative of **HS256** or any symmetric algorithms  is that because you have to distribute the secret to any verifying services, it has a much higher chance of being leaked (it’s a private key that should not normally ever leave the authorization server or a certificate store). This means the trust level is reduced as you cannot say 100% that the token was signed by the authorization server you are expecting.

A final note on symmetric algorithms is that the secret used to create the signature needs to be unique, sufficiently large and complex to prevent it being brute forced. There have also been instances where people have forgotten to change the default secret included with the package or example code they used. These have been compiled into helpful word-lists such as [Wallarm's JWT Secrets list](https://github.com/wallarm/jwt-secrets). This allows attackers to quickly check if your JWT is signed with something from an example rather than a traditional brute force.

## Decode
As JWTs are base64 encoded, they need to be decoded before the information inside can be reviewed and the JWT verified. Important node: **Decode != Verify**

The library you are using may have a decode function included as well. 

Before running the verify function you may choose to validate some of the tokens claims such as the issuer if you need to support more than a few. Most verify functions will take in a list of issuers but you may have too many to pass. It may be better to verify the issuer yourself and then pass the issuer in as an additional option for the verify function.

If you are doing RS256 you will want to check if the key Id that is present in the header matches the key Id you have. If it doesn't then the verify function will fail. There may have been a genuine key rotation meaning new tokens have a new key you don't have (or it's a bad token).  Retrieve the keys from the JWKS URI and check if the key matches, if multiple keys are present but neither matches, reject the token. If one does match, optionally save it, then send it to the verify function.

**Do not act on the information in the token until it's verified.**

## Verification
Verification ensures that the token is signed correctly and that it's currently valid for use. 

Where verification often fails due to missing or incorrect configuration: 

* Algorithms are not defined. As above there should only be one algorithm in use which you can pass to the verify function.
* Issuers are not checked. You know who can issue tokens for your service. Ensure that you are passing that issuer into the function as it will be able to check if that was the claim versus your expectation. You may have already checked this as well once it was decoded.
* Audience isn't checked. You may have multiple APIs. Ensure that the tokens received are for the API that is being called.
* Scopes are not checked. You may have scopes providing coarse grained authorization on the API. Check that the scope(s) for the resource that is being requested are present.

You can read what Auth0 has to say about validation here:
* [Validate JSON Web Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens)
* [Validate Access Tokens](https://auth0.com/docs/secure/tokens/access-tokens/validate-access-tokens)

## Rotate your keys
You should rotate your keys to prevent brute force attackers from cracking the key or secret used to sign the tokens. For asymmetrically signed tokens, this should be fairly trivial as you will likely have implemented a key retrieval mechanism. If you receive a new key, you would need to trigger that.

For symmetrically signed tokens this is much harder as you need to provide the secret to all services via a secure back channel. Note that some identity providers use symmetrically signed tokens for refresh tokens as they are the sole consumer of them so already have access to the secret.

Be careful to provide a long enough overlap depending on the lifespan of your tokens. You wouldn't want to reject a legitimate token that has not expired yet.

[Okta's fantastic write up on key rotation](https://developer.okta.com/docs/concepts/key-rotation/)

## Summary
* Read the TLDR
* Choose a single algorithm
* Decode and validate
* Validate issuers against a known list or a rule set.
* Don't act until it's validated
* Test it in dev and prod

<p align="center">
<img  src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipcfvhyx2aodgo662712.png" alt="Picture of Sergeant Phil Esterhaus's (played by Michael Conrad) morning roll call catchphrase in 'Hill Street Blues'. Text reads Let's Be Careful Out there" width="400"/>

## Research links
* [JWTs? JWKs? ‘kid’s? ‘x5t’s? Oh my! | Red Thunder](https://redthunder.blog/2017/06/08/jwts-jwks-kids-x5ts-oh-my/)
* [JWTs: Which Signing Algorithm Should I Use? | Scott Brady](https://www.scottbrady91.com/jose/jwts-which-signing-algorithm-should-i-use)
* [Navigating RS256 and JWKS | Auth0](https://auth0.com/blog/navigating-rs256-and-jwks/)
* [JWT Security Best Practices | Curity](https://curity.io/resources/learn/jwt-best-practices/#5-always-check-the-audience)
* [7 Ways to Avoid JWT Security Pitfalls | 42Crunch](https://42crunch.com/7-ways-to-avoid-jwt-pitfalls/)
* [Validate Access Tokens | Auth0](https://auth0.com/docs/secure/tokens/access-tokens/validate-access-tokens)
* [Validate JSON Web Tokens | Auth0](https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens)
* [Key Rotation | Okta](https://developer.okta.com/docs/concepts/key-rotation/)