ruk·si

✒️ JSON Web Token (JWT)

Updated at 2024-02-25 15:00

JSON Web Token (JWT) is an open standard defining a self-contained way for securely transmitting information between parties as a JSON object. The core idea is that the ones delivering the message can read it but can't modify it.

JWT is mainly used for authentication and authorization. Technically, JWTs can be used to transfer any information that must not change in transit.

You have a matching passport that tells who you are. (authentication)

Your passport lists your age, which allows you to buy alcohol. (authorization)

The main benefit of JWT is the global adoption. There are multiple JWT libraries for all the major programming languages. Many authentication servers support JWT out of the box like AWS Cognito.

JWT authentication is straightforward, but cutting corners makes it insecure.

JWT setup using asymmetric keys without key rotation and long-lived tokens is a recipe for disaster.

JWTs are stateless. Services can verify JWTs independently without having to access the authentication server or your database. Works great with stateless services.

JWTs consists of three unencrypted parts separated by dots; {1}.{2}.{3} where:

  1. JOSE Header: JSON Object Signing and Encryption header is base64Url encoded JSON that describes e.g. the type of the token in typ (JWT) and hashing algorithm used in alg.
  2. JWS Payload: JSON Web Signatures payload is base64Url encoded JSON that contains claims. Claims are statements about the entity or additional data.
  3. JWS Signature: JSON Web Signatures signature is used to verify the sender and to ensure that the header or the payload hasn't been changed along the way.

JWT is not an encryption 🙈

JWTs are like passwords and should be treated such. JWTs should always be communicated over HTTPS or similar.

JWT contents are not encrypted; their integrity is just verified. This means you should not put secret information in the token payload. Everyone can read what is the actual payload.

Assume that anything written to the JWT will be read and used by users. You should handle JWT contents like any API response and be backwards compatible.

Always use asymmetric keys. Here are top algorithms to use, pick one labeled Recommended that your libraries support, as of 2024:

  1. ECDH-ES: ECDH-ES using Concat KDF
  2. RSA-OAEP: RSAES OAEP using default parameters
  3. ES256: The Elliptic Curve Digital Signature Algorithm (ECDSA) using P-256 and SHA-256

Use HS256 (HMAC using SHA-256) if you require a symmetric key, but this is less secure as now all services can write tokens like an auth server could.

A good rule of thumb is to use as long signing key as the hash output. For example, 256 bits of random for HS256.

On the first login, a client is usually issued two tokens: an access token and a refresh token. Subsequently, the refresh token is periodically sent to your auth service to get a new pair of tokens. But many JWT libraries handle this automatically.

  • access token provides authentication and authorization details
  • refresh token provides authentication and allows getting access tokens
  • optional identity token provides details about the owner like email

You should always validate incoming JWTs. You can only skip the validation when you receive a JWT from the authentication server over HTTPS.

Whitelist signing algorithm in alg header claim. An attacker can intentionally use a weaker algorithm like none.

Whitelist issuer claim iss. Check that the signing key used belongs to exactly that issuer. E.g. if you're using OpenID Connect, the issuer must be a URL using the HTTPS scheme. This makes it much easier to confirm the ownership of the keys. Thus, it's a good practice to always use such URLs as the issuer value.

  • token claim iss is the auth server URL that generated it

iss: https://sso.example.com

Whitelist audience claim aud. The resource server should always check the audience aud claim in the token and make sure it is part of the audience.

  • access token claim aud is URL(s) of the API its intended for
  • refresh token claim aud is the auth server URL
  • identity tokens don't have aud as they are only for the client

aud: https://api.example.com

You should have an expiry on your JWTs, the exp claim. Infinite authentication tokens are dangerous as they can be stolen and used indefinitely. You should either have extension endpoint for users manually to refresh with a valid token or have a separate long-lived refresh token.

Honor exp and nbf claims yourself. iat can be an extra check for ancient or from future. Up to 30 second skews are possible, but a few seconds are more common.

JWT exp should be hours or less. 2 - 15 min is most common. Never days or above. The reason is that permissions can be revoked only on token refresh.

Because of the authorization change lag, JWTs are probably not the best for real time financial or high security cases where authorization granularity is needed. Session auth with the possibility of session clear is probably better.

Identify client subjects with claim sub. Also, jti claims are sometimes used to obliterate the chance of duplicate JWTs, as a same client could generate it twice on the same second. This can be an issue if you have refresh token rotation and two different clients get the exactly same refresh token.

Rotate your signing keys. It's a good practice to always use an endpoint and dynamically download the signing keys from the authorization server (caching responses accordingly to what the server returns in the cache control headers). This allows for an easy key rotation, and any such rotation will not break your implementation.

Do not use plain JWT for browser sessions. They are not meant for that and are less secure. Use session cookies like normal.

Although you can use JWTs for sessions if you store and send them like a normal session cookie, it forces you to create a long-lived token and thus is less secure. But not less secure than a normal session token; but neither more secure as long as both are hard to guess 🤷

One of the best uses of JWTs is a single use token for a single API call.

Do not use JWT as a database. HTTP headers have an 8 kb limit. So you can store about 5 kb of data in a JWT token.

Consider token family invalidation. All access tokens and refresh tokens that are issued from the same original refresh token are considered a token family. If you record the same refresh token being used twice, you should consider all tokens in the family invalid. Even the ones that were generated previously as you can't know which holder is the fraudster.

Sources