This post will be about JWTs for web development, not about mobile apps, programs or embedded devices

What is JWT

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.

And for humans?

Basically, JWT is token which content is visible to everyone, but no-one can change it’s content, since it’s signed by issuer*.

The suggested pronunciation of JWT is the same as the English word “jot”

Which sounds kinda odd, for at least for non native speakers..

In general JWT consists of 3 parts

Header - contains metadata, like who signed JWT and how long it is valid

Body - content of JWT, like user roles, permissions, basically just some custom data

Signature - signature that is used to validate that no-one changed JWT contents

All those parts are separated with a “.” (dot)

Example JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ. 6xWqaqNdCsyhIjc32MJKfikpOhAaBG9mz93He-E3Hvs

Since JWT is plaintext and everyone can read it, one can copy-paste JWT to https://jwt.io/ and see what’s inside of it, without any programming knowledge.

Everything in header is mostly metadata. Who is JWT aimed at, who is allowed to use it, what algorithm is used for signing and validation, how long is JWT valid etc

Type (typ) - Optional parameter to set media type. It’s recommended tho to set it to “JWT”

Algorithm (alg) - Algorithm that is used to sign and validate JWT. Setting it to “none” will remove signing

Issuer (iss) - Optional field to mark who signed this JWT. Usually something like auth.example.com or just example.com

Subject (sub) - Optional subject to who or what this JWT belongs to. Usually user email or UUID

Audience (aud) - Optional single string or array of strings that represent audiences who can use given JWT. For example [“google.com”, “mcsneaky.ap3k.pro”]

Not Before (nbf) - Optional time before what JWT must not be accepted

Expiration Time (exp) - Optional expiration time for JWT, after JWT is expired it must not be accepted anymore

Issued At (iat) - Optional time when JWT was issued at

JWT ID (jti) - Optional JWT ID, it must be unique across all JWT IDs used in system. Good for caching JWTs

Key ID (kid) - Optional key ID, good when rotating keys and need to keep track which key was used to generate given JWT

Body / payload

JWT body can be whatever, it can be whole database or just key-value pair of one thing. Only requirement is that it is JSON parsable

For example: {"hurr": "durr"} is valid JWT body


Signature is made using JWT header and body. How exactly signature is signed depends on alg given in header of JWT.

If alg is set to none JWT will not be signed nor validated and anyone can edit it’s content however they like, which is huge security breach. That’s why “none” algorithm should always be disabled

That’s why there was * in:

but no-one can change it’s content, since it’s signed by issuer*.

When alg is none JWT will be missing signature altogether and then JWT will contain only two parts - header and body

Refresh token

Since JWTs are short lived, usually some minutes, it would be annoying for user to log back in every 5 minutes. That’s where refresh tokens come to the rescue. Since refresh tokens have lifetime of several hours to several months they are good to reduce user annoyance.

When user signs in issuer will generate both JWT and refresh token and hurl them both back to client. For all requests JWT will be attached, but when JWT expires, then client will send refresh token back to issuer to get new JWT in return.

Refresh tokens are not portable like JWTs. Refresh token relies on issuer to have exactly the same token saved into somewhere to validate it (usually in database)

JWTs are short lived and there is not so much of a risk at JWT breach comparing to refresh token breach. Refresh token defines client, if that token is stolen it can be used to generate new JWTs by hackers even if original JWT has been long expired.

Refresh token must be kept as secure as possible

Some tips for using JWTs

Where to keep them

If JWT is added to request headers Authorization header should be used for it and token type should be set to JWT or to bearer. According to Passport JWT is recommended since it’s more explicit.

Generally it’s good idea to keep JWT inside of a http secure cookie, so it’s only present for https traffic and it is not accessible to JavaScript. There are several other options, like using session / localStorage, but they all are accessible to JS so they might be read off from there and stolen.

HTTPS cookie won’t guarantee that JWT can’t be abused, it will just make it harder. When hacker can inject custom JS to page through XSS or some other method they can make requests from user browser directly. Since domain will be the same and cookies get attached automatically with every request malicious JS requests on same site will be authenticated too.

When JWT is in JS readable storage attacker could take JWT and use wherever, not only on site itself

How to prevent hacks

none alg should always be disabled, since like mentioned above, setting signature algorithm to none allows everyone to fiddle around with JWT content. Signatureless JWT is valid JWT when alg is none

Ensure that when alg is changed from RSA to HMAC then RSA public key is not used as HMAC secret, since it might open up theoretical attack vector and will allow attacker to sign JWTs himself with public key.

Theoretical since at the time of writing has been no known breaches using this method

It’s generally good idea to keep rotating RSA key pairs slowly, for example one new key every month. When new key pair is generated oldest key pair should be deleted. Rotation speed depends on usage and tokens lifetime. There is no general recommended rotation speed.

When rotation is used JWT header should contain kid (key id) to ensure JWT is validated against right public key.

Issuer can expose API endpoint to get list of public keys (or single key) and then kid is pointing at key ID. For example Google has them exposed in here: https://www.googleapis.com/oauth2/v1/certs and they keep history of 2 keys while rotating

Moving between services

When user is moving between services user might need new JWT based on from and to he or she is moving.

Usually JWT exchanges are built for it and they are mostly used to get user roles / permissions in other service

For example when user is administrator of one blog he or she might have only read access in central forum. When user is moving across such services it’s generally better to exchange JWT than to bundle permissions of all possible services together into single JWT.

Since it will make JWT bigger and will increase network overhead.

How to log out

When user logs out refresh token will be invalidated (deleted) by issuer, so refresh token can no longer be exchanged for valid JWT.

All tokens must be deleted from user device / browser, so user can’t make any more requests and has to sign in again to get new tokens.

For instant logout across all services JWTs existing JWTs must be invalidated

How to revoke tokens

JWTs are stateless and they can’t be revoked

States every other tutorial and blog post, which is only half of the truth.

Actually JWTs can be “revoked”, but it’s not as straightforward and it’s more like blacklisting.

When JWT is sent to external service, there is really no way to invalidate JWT unless external service has integrated client blacklisting.

When issuer invalidates some client JWT (for example user account gets closed) then issuer will have to send out event to all services that JWTs of certain client has been revoked. Also all refresh tokens related to this client must be deleted / invalidated to ensure refresh tokens can not be used to get new valid JWT.

All services should be listening to client invalidation event by providing webhook URL for issuer to post this event or by listening event off from general event bus, like Kafka for example.

Event payload will contain user UUID and timeframe in which timeframe all JWTs will be invalid.

Now when client makes request with JWT to some service, service shall first check if JWT has been blacklisted or not. If JWT is blacklisted, client would fallback to asking new JWT from issuer using refresh token but since refresh token has been revoked too user is essentially logged out.

Like mentioned, this only works when all services react to invalidation event, if some service does not have blacklisting option JWT can not be revoked

Huge red panic button

When there is some breach it’s good to have huge red panic button that will invalidate all refresh tokens (and possibly revoke all JWTs). It will help to keep size of breach smaller. It’s annoying for users when they have to sign in again, but better to annoy users than let data breach escalate to something bigger and more serious.

Deleting clients

Deleting clients across services works quite the same as revoking JWTs. When issuer sends out delete event, then all services should listen to it and delete client related data.

Deleting can be wrapped into cross service migration. In such case issuer will send out event that client is about to get deleted, this will start delete migration in all services.

When migration is successful service should report back to issuer, that migration was all green. Issuer collects all postbacks and when every single service migration was a success another event will be sent out, that tells services to commit migration.

In case one or more service reports that migration failed issuer will send out event that will tell all services to rollback migration.

Password resets, 2FA etc

This depends quite a lot on business requirements.

In general for such cases when PW reset or 2FA authentication first step happens huge random token is returned to client that can’t be used for anything else than exchanging this with some additional data to JWT

For example in 2FA first client request will be username and password Issuer will respond with huge random token and will send out push event to some provider or send SMS or email etc Client will send huge random token back to issuer with code received from other source Issuer will give client JWT and refresh token in exchange for that temporary token

Quite the same works with password reset, in that case new password is required instead of 2FA code