DEV Community

Cover image for Safely Handling JWTs
Alex Savage for Advanced

Posted on • Updated on

Safely Handling JWTs

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.

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.
  • 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) 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.

  • 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


An example JWT from https://jwt.io

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.


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:


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

  • Signature algorithm: In our example, RS means RSASSA-PKCS1-v1_5 (typically)
  • Hashing algorithm In our example, 256 means SHA-256

    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 defines a list of 13 options (One being “none” where the JWT has no signature at all!)


List of supported JWT signing algorithms taken from RFC7518

In addition to the RFC, there are others available which you can find on the iana registry: JSON Object Signing and Encryption (JOSE)

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) 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. 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:

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

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


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

Research links

Oldest comments (3)

Collapse
 
matthewt profile image
Matt Tanner

JOTs sound so friendly. Surely they are secure and generally a Good Thing? You quite soundly debunk all of that. Nice TL;DR too.

Collapse
 
uithemes profile image
ui-themes

Nice & Complete content.
TY!

Collapse
 
chalermporn profile image
Chalermporn Posoppitakwong

Wow