I had to write a JWT authorizer for work recently and thought it would be a great time to finally learn more about them! JWT (JSON Web Token) is a standard for representing and exchanging identify-based information between a client and a server. Itā€™s a base64url-encoded string that contains assertions (or claims) about the user thatā€™s trying to access something on a resource server. JWTs can be used in both authentication (verify the user is who they say they are) and authorization (what resources a user is allowed to access).

šŸ”Ž What does a JWT look like?

A JWT is made up of 3 base64url-encoded segments, each separated by a dot .:

Letā€™s break this down! šŸ’ƒšŸ»

This is the first segment in the token:

Which decodes to:

{
  "alg": "HS256",
  "typ": "JWT"
}

The header contains information about the token, which typically includes the token type (JWT in this case) and the algorithm used to sign the token (HS256).

You might also sometimes see a Key ID (kid) in the header, which is just an arbitrary, case-sensitive string used to retrieve the key thatā€™s required to verify the JWT signature. This can be useful for situations where there are multiple keys in a key store.

Payload

This is the middle segment:

Which decodes to:

{
  "id": "1234567890",
  "name": "Jane Doe",
  "iat": 1516239022,
  "exp": 1678401985
}

The payload contains information (claims) about the user and anything else you want. The JWT specification technically imposes no limit to how much or what information you can store in a JWT, but you might be restricted by the constraints set by the server receiving the token and the level of information security you require (more on this later).

Signature

This is the last segment of the token:

The signature is a cryptographic hash thatā€™s used to verify the information in the JWT have not been tampered with in transit. To create one, you take the tokenā€™s payload and header (collectively called the signing input), hash it, and then sign the hash. There are many signing algorithms, but the most common ones are:

With HMAC algorithms (prefixed with HS), the signing input is hashed using SHA256 and signed using a secret known to the tokenā€™s creator and recipient. RSA (prefixed with RS) and ECDSA (prefixed with ES) algorithms are used to generate asymmetric signaturesā€“where the hash is signed using a private key, and validated using a corresponding public key. RSA and ECDSA offer similiar levels of security, except with ECDSA you have the added bonus of smaller key sizesā€“which results in smaller JWTs as well (something to consider if performance is important).

HMAC is easier to implement but poses security issues if the secret is stolen, because the secret can be used to sign and validate tokens. RSA and ECDSA are more secure in comparison because the private key is not shared and only the party with the private key can sign tokensā€“this offers a way for token recipients to verify that tokens are created and signed by a trusted party.

The JWT signature is important because this is how the recipients of the token can verify that the token has not been tampered with in transit. As mentioned before, the signature comprises of the header, payload, and a secret or key. When a JWT is received by the resource server, it will take the tokenā€™s header and payload and generate its own hash using the algorithm in the JWT header. The generated hash is compared with the received hash, and if thereā€™s a match, the token is considered valid and access is granted. (Note that in practice you donā€™t have to implement any of this logic yourself, as there are many libraries out there that can handle this for you!)

šŸ£ Where do JWTs come from?

Although you can certainly generate your own JWTs with the help of libraries (e.g. the jsonwebtoken NPM package), JWTs can also be issued and signed by a dedicated server called an authorization server. You can use servers provided for you by companies like Okta or you can create your own.

Authorization servers provide a public API that your application can use for tasks like requesting and refreshing tokens. For signature validation, the authorization server stores public keys and/or symmetric keys in a JSON structure known as a JSON Web Key Set (JWKS), which is accessible via the API as well.

Using a dedicated server for issuing and managing tokens greatly simplifies the software development process because itā€™s one less thing you have to worry about when building your application. In a microservices architecture, multiple applications can use the same authorization server. It also offers more fine-grained control because authorization servers and application servers will have different security and resource requirements.

šŸ”Œ How are JWTs used?

Youā€™ll often see JWTs being sent as bearer tokens when making API requests:

curl -iv -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMz
Q1Njc4OTAiLCJuYW1lIjoiSmFuZSBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTY3ODQwMTk4NX0.mFw
PTU2wS60pWu1dI3TS7ytzWFysHsMhN1HeFcwJ0t0" https://localhost:8080/api

This mechanism is how the resource server knows what resources youā€™re allowed access to, and what actions you can take (if any).

JWTs are a key part of OAuth 2.0 and OpenID Connect (OIDC).

OAuth 2.0 is a specification for authorization, allowing an application (the client) to access the userā€™s resources on a resource server. You, the user, already have access to both the client and the resource server, but with OAuth, you are allowing the client to access the resource server for you.

The client starts by asking you for access to the resource server, if you agree, an authorization code is returned. The client would then send a request to the authorization server and exchange the authorization code for an access token. The access token represents the permissions that the client has been granted and the token will be included in the header of every request to the resource server. This flow would also be familiar to you if youā€™ve ever had to get access to use a third party API.

Although access tokens can come in the form of JWTs, they can also just be opaque tokens/a random string instead. Some people like to use JWTs because the token contains all the information they need to validate someoneā€™s permissions without having to make requests to an external service (e.g. a database) like you might have to when using opaque tokens.

OpenID Connect (OIDC) is based on OAuth 2.0, except itā€™s a means of authentication. This protocol allows you to implement federated identity managementā€”where you can use one set of credentials to authenticate to many different applications. The flow is similar to OAuth2.0, except the client makes a request to an identity provider (which is just an authorization server that supports OIDC) to retrieve an ID token and optionally, an access token. The ID token is sent back to the client as proof that the user has been authenticated.

Unlike access tokens, ID tokens must be JWTs and also contains information about the authentication process such as the tokenā€™s intended recipient, token issuer, etc. The token can include basic user info as well (name, email, etc), although itā€™s not required by the OIDC spec.

šŸ’­ Considerations

One major thing to be aware of is that the JWT is valid until it expires. This means even if youā€™ve deleted the JWT from your server/database/cache, the user will still have permissions granted by the JWT until it expires. For this reason, itā€™s a bad idea to use JWTs as session tokens.

Secondly, a key attribute of bearer tokens is that anyone who has it can use it. This obviously becomes an issue if the token is stolen, because as long as it can be validated by the resource server, access will be granted. This isnā€™t a JWT-specific problem, but because they are sometimes used as bearer tokens, itā€™s still something to be aware of. Luckily, there are some protections against this, the first is to ensure your tokens are short-lived by setting the exp claim, the second is to ensure you always make requests over HTTPS. Finally, you can also implement some type of sender constraint so that the sender has to offer some proof of their identity before their token can be used.

As mentioned earlier, there are no official restrictions for what you can include in the tokenā€™s payload. However, remember that that JWTs are often just base64-encodedā€“they are not encrypted! Also, while token validation ensures that the information was not tampered with in transit, thereā€™s no mechanism to prevent it from happening, nor does it prevent someone else from using your token. So donā€™t store sensitive information in JWTs and donā€™t rely on it for sending important information!