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)
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
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.
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
but no-one can change it’s content, since it’s signed by issuer*.
none JWT will be missing signature altogether and then JWT will contain only two parts - header and body
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
According to Passport
JWT is recommended since it’s more explicit.
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
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 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
2020-02-19 00:39 +0000