JSON Web Token (JWT, often pronounced “jot”) is a powerful tool for confidently transmitting data between two parties through tokens. These parties can consist of users, servers, or any other combination of services. Based on an open standard (RFC-7519), JWTs are digitally signed with an encryption algorithm, so the receiving party can trust the information contained within. In computer security this concept is known as Data Integrity.
One main benefit of using a JWT is that it’s very compact (assuming the issuer uses JWS Compact Serialization, which is recommended). They are generally small enough to be sent through a POST request, in an HTTP Header, or even as a query string within a URL. However, the more claims you add to a JWT, the more bloated it becomes. You could theoretically create a JWT that exceeds the maximum length of a URL (~2000 characters), but don’t do that! It’s best practice to only add information that the receiving party needs.
JWTs are also self-contained, meaning that a JWT can neatly encompass:
- identifying information about a user
- what a user can access
- an expiration date
- a signature for content validation
- any other serializable information
Keep in mind that anyone can decode the information contained in a JWT without knowing the private keys. For this reason, you should never put secret information like passwords or cryptographic keys in a JWT. The purpose of a JWT isn’t to encrypt data so it can’t be read during transport (that’s SSL), instead it allows the receiving party to trust that the data received was unaltered during transport. With that out of the way, we can dive into what makes up a JWT.
Structure of a JWT
Every JWT is generated with the same structure. There are three parts, separated by a period. Each section is comprised of base64url-encoded JSON containing specific information for that token. Let’s break down each section below:
The first section is known as the header. This is where a few key pieces of information are contained:
- alg: the algorithm used to sign the token (e.g. HS256 for HMAC SHA-256, or RS256 for RSA SHA-256). RS256 is recommended because it uses asymmetric (public/private) keys instead of relying on a shared private key.
- x5t: an optional certificate thumbprint containing a base64url-encoded SHA-1 of the X.509 certificate corresponding to the encryption key used.
- jku: the JSON Web Key (JWK) url. We’ll cover this in the JWK(S) section below.
- kid: an optional parameter indicating which encryption key was used. This can be used as a signal to recipients that a key was changed.
- typ: the type of token. This parameter is completely optional, however if preset, it’s recommended that the value be “JWT” (always capitalized)
This isn’t a comprehensive list of all the parameters you can find in this section, but is instead a highlight of some more common parameters you’ll likely encounter. You may have noticed the overall theme of this section is encryption — which is why you may also see this section referred to as the JSON Web Encryption (JWE) header.
The second section is the payload (also known as claims), where the JWT issuer can store custom information for the receiving party. There are three types of claims that can be used in the payload: Public, Private, and Registered.
- Public Claims must be collision-resistant — so they should be extremely unique to ensure no two public claims can have the same name. All public claims should be defined in the IANA JSON Web Token Registry or defined as a URI that contains a unique namespace.
- Private Claims on the other hand do not have to be collision resistant, and can be named anything as long as the issuing and receiving party both agree on the use of the claim. It also cannot conflict with a public or registered claim for obvious reasons.
- Registered Claims are universally defined claims which are reserved for specific purposes. Some common Registered Claims are:
- aud: token audience (who the token is intended for)
- exp: token expiration in NumericDate format.
- iat: time the token was issued (issued at)
- iss: token issuer
- jti: unique identifier for a token
- nbf: time before which the token should not be accepted (not before)
- sub: token’s subject
The final section is the signature. This is what makes a JWT secure and ensures the integrity of your JWT during transport. The signature is simply a hash of all the content that was generated with the JWT. That means if any part of the JWT changes, the signature will be invalidated — rendering the JWT malformed. A JWT is signed with a JSON Web Algorithm (JWA). The algorithm for generating a RS256 signature is shown below:
base64UrlEncode(header) + "." + base64UrlEncode(payload),
As you can see, the algorithm takes the base64url-encoded header and payload, concatenated with a period, and signs it with a secret key that is only known to the service that generated the JWT. You may also see this referred to as the JSON Web Signature (JWS).
- Important Note: Always validate your JWTs on the receiving end. Given that claims in a JWT are simply base64url-encoded, you can decode a JWT without actually validating where it came from, or whether it was properly signed. If you don’t validate JWTs when decoding, anyone could send your application a custom-made JWT, rendering the security useless. Most JWT packages and middleware handle validation for you automatically, but don’t assume that they all will.
If your issuer uses RS256 (or another asymmetric algorithm) to sign tokens, you’ll require a public key to validate the token. But how do you get the public key? Great question! And a perfect segue to our next topic.
The JSON Web Key (JWK) is a JSON object that contains a well-known public key which can be be used to validate the signature of a signed JWT.
If the issuer of your JWT used an asymmetric key to sign the JWT, it will likely host a file called a JSON Web Key Set (JWKS). The JWKS is a JSON object that contains the property keys, which in turn holds an array of JWK objects. See an example of a JWKS below:
The service may only use one JWK for validating web tokens, however the JWKS may contain multiple keys if the service rotates signing certificates. The endpoint for retrieving a JWK(S) can vary and should be documented for your issuer. For example, the the standard location for auth0 is: https://YOUR-TENANT.auth0.com/.well-known/jwks.json. Okta, on the other hand makes use of jwks_uri metadata for storing the endpoint. Others may make use of the jku JWE header parameter discussed above.
Any time your application validates a JWT, it will attempt to retrieve the JWK(S) from the issuer in order to ensure the JWT signature matches the content. This process is made simple with open source packages like node-jsonwebtoken.
A JWT is usually attached to a HTTP request via the HTTP Authorization header as a Bearer token. See below for an example (line-breaks added for readability):
The JWT will have to be sent with every request to the backend, which is a tradeoff to consider. The great benefit of this approach is that this provides a stateless form of authentication since the server doesn’t have to remember the user’s information in session storage, significantly reducing the amount of work required to manage that state on the backend. A drawback is that since JWTs are stateless, you cannot invalidate them without storing session state. JWTs will automatically be invalidated after their expiration date, but depending on how long the expiration was set for (10 hours is common), a user could retain access to a service after being removed.
Putting it all together
Understanding the theory behind generating and using a JWT is great, but I always find a practical example helps solidify understanding. So, I’ll leave you with a diagram that shows a typical flow where a JWT is used:
I want to clarify steps 4–7 by pointing out that it would be odd to send a JWT to a server simply for the purpose of sending a JWT. The JWT should accompany an actual protocol request, like an HTTP Request (POST/PUT/GET/DELETE), as a voucher for that specific request. Moreover, by living on the Authorization header, it keeps your query strings and payloads clear of extra data so they can stay neat and focused.
Thanks for reading, and please leave some claps if you found this useful!