You don’t know JWT

Explore the essentials and advanced nuances of JSON Web Tokens (JWT) in our blog series "You Don't Know JWT." Learn about JWT security, implementation, and common challenges.

As a software engineer with some experience, there are certain concepts and technologies that I have been using for years but do not fully understand. One such concept is JWT. After delving deeper into it, I realized that there is a lot I don't grasp yet. Therefore, this blog aims to explore the world of JSON Web Tokens together. Let’s begin.

Note that all the information I share in this blog is from my perspective, after conducting thorough research on the internet. Therefore, it may not be entirely accurate.

JWT (JSON Web Token) is a simple, secure, and standard way of exchanging information between two parties (client-server, server-server, etc.). It’s widely used in authorization

Even though JWT can be used in various ways, the most common use case where it’s used is for authorization. Some advantages of JWT make it very convenient for authorization

  • Session Management: Traditional session-based authentication requires servers to store session data for each authenticated user, which can lead to scalability and performance issues, especially in distributed systems. JWT eliminates the need for server-side session storage by storing authentication information directly within the token itself.
  • Cross-Origin Authentication: In distributed systems where services are hosted on different domains or subdomains, traditional session-based authentication can be challenging to implement due to cross-origin restrictions. JWT tokens can be easily transmitted between different domains and verified by multiple services, making cross-origin authentication more straightforward.
  • Statelessness: JWT tokens are stateless, meaning they contain all necessary information for authentication and authorization. This statelessness simplifies server-side logic and allows for horizontal scaling of services without the need for shared session storage.
  • Microservices Architecture: In microservices architectures, where applications are composed of multiple independent services, managing authentication and authorization across services can be complex. JWT tokens provide a unified authentication mechanism that can be shared across services, enabling seamless communication between microservices.
  • Security: JWT tokens can be digitally signed and encrypted to ensure data integrity and confidentiality. This protects against tampering and eavesdropping, enhancing the overall security of authentication and authorization processes.
  • Standardization and Interoperability: JWT is based on open standards (RFC 7519), making it widely adopted and interoperable across different systems and frameworks. This standardization promotes consistency and compatibility among applications using JWT for authentication and authorization.

I'm not sure I can call them a "type" of JWT. However, JWT exists in two forms: JWS (JSON Web Signature) and JWE (JSON Web Encryption). In most circumstances, JWT will appear in the form of JWS.

So, what is the difference between them?

  1. JWS (JSON Web Signature):
    • What: JWS is a standard for digitally signing JSON data.
    • Why: Ensure data integrity and authenticity, used in authentication and message integrity scenarios.
    • How: Create a compact or JSON representation of a token that includes a signature, which can be verified by recipients to ensure that the data has not been tampered with.
  2. JWE (JSON Web Encryption):
    • What: JWE is a standard for encrypting JSON data to provide confidentiality?
    • Why: Protect the confidentiality of the data, such as in data encryption and secure communication scenarios.
    • How: encrypt the payload of a token using a specified encryption algorithm and key, resulting in a token that can only be decrypted by authorized recipients with the corresponding decryption key.

⚠️ Note that the JWTs are always represented using the JWS Compact Serialization or JWE Compact Serialization. With other serialization forms, they’re not called JWT. The Serialization Forms for each type will be mentioned in the next sections.

JWS Serialization refers to the process of representing the JWS in a specific format for transmission or storage. There are 3 main serialization formats for JWS:

  • Compact Serialization: This format represents the JWS as a single string consisting of three Base64URL-encoded parts separated by periods (.). It is commonly used in scenarios where space efficiency is important, such as URL parameters or HTTP headers. This is also the most common serialization type used in real-world applications. Example:

    bash
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
    TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
    TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  • JSON Serialization: In this format, the JWS is represented as a JSON object containing separate properties for the JWS header, payload, and signature (if applicable). Each property holds a Base64URL-encoded string representing the corresponding part of the JWS. JSON Serialization provides a more structured representation of the JWS and is often used in JSON-based APIs or when additional metadata is needed. Example:

    json
    {
      "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogIm
                  h0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      "signatures": [
        {
          "protected": "eyJhbGciOiJSUzI1NiJ9",
          "header": {
            "kid": "2010-12-29"
          },
          "signature":
            "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AA
            uHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAyn
            RFdiuB--f_nZLgrnbyTyWzO5vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB
            _eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6
            IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlU
            PQGe77Rw"
        },
        {
          "protected": "eyJhbGciOiJFUzI1NiJ9",
          "header": {
            "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d"
          },
          "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDx
                        w5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
        }
      ]
    }
    {
      "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogIm
                  h0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      "signatures": [
        {
          "protected": "eyJhbGciOiJSUzI1NiJ9",
          "header": {
            "kid": "2010-12-29"
          },
          "signature":
            "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AA
            uHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAyn
            RFdiuB--f_nZLgrnbyTyWzO5vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB
            _eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6
            IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlU
            PQGe77Rw"
        },
        {
          "protected": "eyJhbGciOiJFUzI1NiJ9",
          "header": {
            "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d"
          },
          "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDx
                        w5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
        }
      ]
    }
  • Flattened JSON Serialization: This format is similar to JSON Serialization but combines the JWS header and the protected header parameters into a single "protected" property. This results in a simplified JSON object with fewer top-level properties. Flattened JSON Serialization is useful for reducing redundancy and improving readability, especially in cases where the JWS header is small or static. Example:

    json
    {
      "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog
                  Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      "protected": "eyJhbGciOiJFUzI1NiJ9",
      "header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },
      "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFC
                    gfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
    }
    {
      "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQog
                  Imh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
      "protected": "eyJhbGciOiJFUzI1NiJ9",
      "header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },
      "signature": "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFC
                    gfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q"
    }

Compact serialization is the most commonly used format for JWS tokens. Due to the breadth of possible serialization forms, this section will focus solely on the JWS token in its compact form.

From this point forward, the term "JWT Token" will specifically refer to a JWS token in compact form.

A JWT token is comprised of three parts: the header, the payload, and the signature. These components are encoded using Base64URL encoding and are separated by dots.

Example:

java
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

There are a couple of reasons why JWT encodes using Base64URL format:

  • Compactness: Base64URL encoding converts binary data into a text-based format, which is more compact and easier to transmit over the network. This compactness is essential for JWTs, especially when they are included in HTTP headers or query parameters.
  • Compatibility: Base64URL encoding is widely supported by programming languages and frameworks, making it a practical choice for encoding binary data in JWTs. It allows JWTs to be easily decoded and manipulated by various systems and libraries without compatibility issues.

Let’s break down those parts:

The header, which is a JSON object, contains information about the used algorithm, whether the token is signed and how to parse the JWT Token as well

There is only one mandatory claim, which is:

  • alg: the main algorithm in use for signing this JWT. ****If this field is set to “**None**”, that means the token is unsecured.

Other optional claims are:

  • type: the type of the token; most of the time, it’s JWT
    • There are some other values, such as:
      • AT: indicates token is an access token.
      • JWT + JWT: indicates token is a nested JWT
      • Custom value: Custom value by the developer
  • cty: the content type of the token. Most of the time, this field should not be set. In case the payload of JWT is a JWT, this field must be set to "JWT.

An example of common header is:

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

Most commonly used JWT libraries generate the header automatically based on the input of the sign method.

Apart from above common claims, there are some additional claims which are used for some specific cases. Some remarkable claims include:

  • jku: JSON Web Key (JWK) Set URL. A URI pointing to a set of JSON-encoded public keys used to sign this JWT. Transport security (such as TLS for HTTP) must be used to retrieve the keys. The format of the keys is a JWK Set.
  • jwk: JSON Web Key. The key used to sign this JWT is in JSON Web Key format.
  • kid: Key ID. A user-defined string representing a single key used to sign this JWT. This claim is used to signal key signature changes to recipients (when multiple keys are used).
  • x5u: X.509 URL. A URI pointing to a set of X.509 (a certificate format standard) public certificates encoded in PEM form. The first certificate in the set must be the one used to sign this JWT. The subsequent certificates each sign the previous one, thus completing the certificate chain. X.509 is defined in RFC 52807. Transport security is required to transfer the certificates.
  • x5c: X.509 certificate chain. A JSON array of X.509 certificates used to sign this JWS. Each certificate must be the Base64-encoded value of its DER PKIX representation. The first certificate in the array must be the one used to sign this JWT, followed by the rest of the certificates in the certificate chain.
  • x5t: X.509 certificate SHA-1 fingerprint. The SHA-1 fingerprint of the X.509 DER-encoded certificate used to sign this JWT.
  • x5t#S256: Identical to x5t, but uses SHA-256 instead of SHA-1.
  • typ: Identical to the typ value for unencrypted JWTs, with additional values “JOSE” and “JOSE+JSON” used to indicate compact serialization and JSON serialization, respectively. This is only used in cases where similar JOSE-header-carrying objects are mixed with this JWT in a single container.
  • crit: from critical. An array of strings with the names of claims that are present in this same headers used as implementation-defined extensions that must be handled by parsers of this JWT. It must either contain the names of claims or not be present (the empty array is not a valid value).

The payload is a JSON object that contains all the interesting user data. There are no mandatory claims. But there are some fields that are registered that have specific meanings; don’t miss using those fields, otherwise you might get into trouble.

There are 3 types of claims in Payload:

  1. Registered claim: They are predefined claim names that have standardized meanings and usage defined in the JWT specification (RFC 7519). These claims provide useful metadata about the JWT itself or the entity (typically the user) it represents. Registered claims are commonly recognized and processed by JWT libraries and frameworks.
    • "iss" (Issuer): Indicates the issuer of the JWT, i.e., the entity that issued the token.
    • "sub" (Subject): Identifies the subject of the JWT, typically the user or entity for which the token was issued.
    • "aud" (Audience): Specifies the intended audience of the JWT, i.e., the recipients who are expected to accept and process the token.
    • "exp" (Expiration Time): Specifies the expiration time of the JWT, after which the token should no longer be considered valid.
    • "nbf" (Not Before): Indicates the time before which the JWT must not be accepted for processing.
    • "iat" (Issued At): Specifies the time at which the JWT was issued.
    • "jti" (JWT ID): Provides a unique identifier for the JWT, which can be used to prevent token
  2. Public claims: Claims which are widely used in public community but are not registered
  3. Private claims: Claims defined by users for particular cases.

You might notice that all the field name are short. It’s because we want to keep the JWT token as small as possible.

⚠️ What if I have duplicate claims in the JSON payload?

The handling of duplicate claims in a JSON payload varies depending on the JWT library you are using. Some libraries apply a "last-win" strategy, where the last defined value takes precedence. Others might throw an error if they detect duplicates. To avoid unpredictable behavior, it's best to ensure that you do not include duplicate claims in your payload.

Now we come to the final crucial part of JWT: the signature, which ensures the integrity and authenticity of the token.

How is a JWT signature created?

  1. Create a digital signature using the cryptographic algorithm specified in the header, such as HMAC, RSA, or ECDSA. The input parameters for the algorithm include the concatenation of the Base64URL-encoded header and payload, separated by a dot ('.'), along with a secret key.

    For example:

java
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
  1. Encode the created value in step 1 using Base64URL

How does signature help ensure the integrity of the token?

When receiving the JWT, recipients decode the header and payload, concatenate them, and recompute the signature using the same cryptographic algorithm specified in the header and secret key. If the computed signature matches the signature appended to the JWT, it indicates that the token has not been tampered with and is considered valid.

Here is the diagram how JWT token is created

https://auth0.com/resources/ebooks/jwt-handbook

This is an example code of how to create JWT in Javascript using jsonwebtoken library

json
const jwt = require("jsonwebtoken");
const payload = { sub: "1234567890", name: "John Doe", admin: true };

const secret = "my-secret";
const signed = jwt.sign(payload, secret, {
    algorithm: "HS256",
    expiresIn: "5s",
});
const decoded = jwt.verify(signed, secret, {
    algorithms: ["HS256"],
});
const jwt = require("jsonwebtoken");
const payload = { sub: "1234567890", name: "John Doe", admin: true };

const secret = "my-secret";
const signed = jwt.sign(payload, secret, {
    algorithm: "HS256",
    expiresIn: "5s",
});
const decoded = jwt.verify(signed, secret, {
    algorithms: ["HS256"],
});

There are a couple of signing algorithms used in real-world applications.

  • HMAC: use a secret string along with cryptographic hash function to create digital signature.

    • Pros: Simple. The recipient can use the secret string to verify the received token.
    • Cons: The secret must be shared across all the parties; there is a high risk of leaking the token. The hacker can easily create malicious JWT token if they have the secret.
  • RSASSA: Use a pair of key: public key and private key. The private key can be used both to create a signed message and to verify its authenticity. The public key, in contrast, can only be used to verify the authenticity of a message.

    https://auth0.com/resources/ebooks/jwt-handbook

  • Pros:

    • Secure, since the recipients only know the public key for verification. Only the party which produces the token has the private key and is able to generate token
    • The public key can be stored in somewhere, and you can pass the URI in the JWT header → The recipient doesn’t need to store the public key
  • Cons:

    • More complex
    • RSA keys used in RSASSA tend to be larger compared to symmetric key algorithms like HMAC. This can result in larger JWTs

This is a lesser-known JWT token type, I have to admit that I did not know about this one until I researched more about JWT.

Like JWS, JWE has 3 form of serialization

  • Compact Serialization: It’s like JWS compact serialization, but instead of 3 parts, JWE has 5 Base64URL encoded parts, separated by periods (.).

    Example:

    json
    eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
    UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
    i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-
    YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
    zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-
    cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
    AxY8DCtDaGlsbGljb3RoZQ.
    KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
    9hH0vgRfYgPnAHOd8stkvw
    eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
    UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
    i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-
    YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
    zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-
    cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
    AxY8DCtDaGlsbGljb3RoZQ.
    KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
    9hH0vgRfYgPnAHOd8stkvw
  • JSON Serialization: JWE JSON Serialization is the printable text encoding of a JSON object with the following members:

    • protected: Base64-encoded JSON object of the header claims to be protected (validated, not encrypted) by this JWE JWT. Optional. At least this element, or the unprotected header, must be present.
    • unprotected: header claims that are not protected (validated) as a JSON object (not Base64-encoded). Optional. At least this element or the protected header must be present.
    • iv: Base64 string of the initialization vector. Optional (only present when required by the algorithm).
    • aad: Additional Authenticated Data. Base64 string of the additional data that is protected (validated) by the encryption algorithm. If no AAD is supplied in the encryption step, this member must be absent.
    • ciphertext: Base64-encoded string of the encrypted data.
    • tag: Base64 string of the authentication tag generated by the encryption algorithm.
    • recipients: a JSON array of JSON objects, each containing the necessary information for decryption by each recipient. Example:
    json
    {
      "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
      "unprotected": {
        "jku": "https://server.example.com/keys.jwks"
      },
      "recipients": [
        {
          "header": {
            "alg": "RSA1_5",
            "kid": "2011-04-29"
          },
          "encrypted_key":
              "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-
              kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx
              GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
              YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
              cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
              wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"
        },
        {
          "header": {
            "alg": "A128KW",
            "kid": "7"
          },
          "encrypted_key": "6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ"
        }
      ],
      "iv": "AxY8DCtDaGlsbGljb3RoZQ",
      "ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",
      "tag": "Mz-VPPyU4RlcuYv1IwIvzw"
    }
    {
      "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
      "unprotected": {
        "jku": "https://server.example.com/keys.jwks"
      },
      "recipients": [
        {
          "header": {
            "alg": "RSA1_5",
            "kid": "2011-04-29"
          },
          "encrypted_key":
              "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-
              kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx
              GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
              YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
              cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
              wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A"
        },
        {
          "header": {
            "alg": "A128KW",
            "kid": "7"
          },
          "encrypted_key": "6KB707dM9YTIgHtLvtgWQ8mKwboJW3of9locizkDTHzBC2IlrT1oOQ"
        }
      ],
      "iv": "AxY8DCtDaGlsbGljb3RoZQ",
      "ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",
      "tag": "Mz-VPPyU4RlcuYv1IwIvzw"
    }
  • Flattened JSON Serialization: As with JWS, JWE defines a flat JSON serialization. This serialization form can only be used for a single recipient. In this form, the recipients array is replaced by a header and encrypted_key pair or elements (i.e., the keys of a single object of the recipients array take its place). This is the flattened representation of the example from the previous section, resulting from only including the first recipient:

    json
    {
      "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
      "unprotected": { "jku":"https://server.example.com/keys.jwks" },
      "header": { "alg":"RSA1_5","kid":"2011-04-29" },
      "encrypted_key":
        "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-
        kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx
        GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
        YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
        cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
        wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A",
      "iv": "AxY8DCtDaGlsbGljb3RoZQ",
      "ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",
      "tag": "Mz-VPPyU4RlcuYv1IwIvzw"
    }
    {
      "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",
      "unprotected": { "jku":"https://server.example.com/keys.jwks" },
      "header": { "alg":"RSA1_5","kid":"2011-04-29" },
      "encrypted_key":
        "UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-
        kFm1NJn8LE9XShH59_i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKx
        GHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-YxzZIItRzC5hlRirb6Y5Cl_p-ko3
        YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8OtvzlV7elprCbuPh
        cCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-cFPg
        wCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A",
      "iv": "AxY8DCtDaGlsbGljb3RoZQ",
      "ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",
      "tag": "Mz-VPPyU4RlcuYv1IwIvzw"
    }

Just like JWS, in this section of JWE, we will only talk about compact serialization.

Example:

json
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-
YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-
cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw
eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKx-
YxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTP-
cFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.
AxY8DCtDaGlsbGljb3RoZQ.
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.
9hH0vgRfYgPnAHOd8stkvw

JWE Compact Serialization has five parts. As in the case of JWS, these elements are separated by dots, and the data contained in them is Base64-encoded.

The five elements of the compact representation are, in order:

  1. The protected header: a header analogous to the JWS header

    A small note here is that, the JWE introduces 2 news claims in header:

    • enc: defines the *content encryption*** *algorithm* and it should be a symmetric ***Authenticated Encryption with Associated Data (AEAD)*** algorithm.
    • zip: defines the compression algorithm in case we need to compress the JWE
  2. The encrypted key: a symmetric key used to encrypt the ciphertext and other encrypted

    data. This key is derived from the actual encryption key specified by the user and thus is encrypted by it

  3. The initialization vector: some encryption algorithms require additional (usually random) data.

  4. The encrypted data (ciphertext): the actual data that is being encrypted → This is the payload in JWS. Note that the unencrypted payload can be JSON, binary or textual data.

  5. The authentication tag: additional data produced by the algorithms that can be used to validate the contents of the ciphertext against tampering.

This is a lot more complex compared to JWS. Luckily, in real life, the common JWT libraries do the most work for us. Most of the time, the creation of JWE is very similar to that of JWS.

This is an example of creating JWE in Javascript using jsonwebtoken library

jsx
const jwt = require('jsonwebtoken');

// Create a JWE
const payload = { user_id: 123 };
const secret = 'my_secret_key';
const token = jwt.sign(payload, secret, { algorithm: 'HS256', enc: 'A256GCM' });

console.log('JWE token:', token);

Parse and decrypt the JWE
jwt.verify(token, secret, (err, decoded) => {
  if (err) {
    console.error('JWE verification failed:', err);
  } else {
    console.log('Decrypted payload:', decoded);
  }
});
const jwt = require('jsonwebtoken');

// Create a JWE
const payload = { user_id: 123 };
const secret = 'my_secret_key';
const token = jwt.sign(payload, secret, { algorithm: 'HS256', enc: 'A256GCM' });

console.log('JWE token:', token);

Parse and decrypt the JWE
jwt.verify(token, secret, (err, decoded) => {
  if (err) {
    console.error('JWE verification failed:', err);
  } else {
    console.log('Decrypted payload:', decoded);
  }
});

There are a couple of encryption algorithms used in JWE

  1. AES-GCM (Advanced Encryption Standard Galois/Counter Mode):
    • Pros: strong security, efficient performance, built-in integrity protection.
    • Cons: It requires support for authenticated encryption and may not be supported in all environments.
  2. AES-CBC (Advanced Encryption Standard Cipher Block Chaining):
    • Pros: Widely supported, well-understood algorithm.
    • Cons: Vulnerable to padding oracle attacks, this requires additional steps for integrity protection.
  3. RSA-OAEP (Optimal Asymmetric Encryption Padding):
    • Pros: Provides confidentiality with asymmetric encryption and supports key exchange.
    • Cons: slower performance compared to symmetric encryption; key management complexity.
  4. ECDH-ES (Elliptic Curve Diffie-Hellman Ephemeral-Static):
    • Pros: Enables key agreement with forward secrecy and efficient key exchange.
    • Cons: requires support for elliptic curve cryptography; potential compatibility issues.
  5. Direct Encryption:
    • Pros: Simple and efficient; no need for asymmetric cryptography.
    • Cons: Requires secure key management practices, limited to scenarios with shared keys.

The most commonly used encryption algorithm in JSON Web Encryption (JWE) for encrypting the payload of a JWT is AES-GCM (Advanced Encryption Standard Galois/Counter Mode).

This comes from the fact that we can manipulate the JWS token without using the signature.

The “alg” claim in the header can be set to “None”. In this case, the JWS becomes “Unsecured JWS” and it’s 100% valid.

For example:

The initial JWS token is:

jsx
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9
  .vqf3WzGLAxHW -
  X7UP -
  co3bU_lSUdVjF2MKtLtSU1kzU;
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9
  .vqf3WzGLAxHW -
  X7UP -
  co3bU_lSUdVjF2MKtLtSU1kzU;

The initial data is:

jsx
header: {
  alg: "HS256",
  typ: "JWT"
},
payload: {
	sub: "joe"
  role: "user"
}
header: {
  alg: "HS256",
  typ: "JWT"
},
payload: {
	sub: "joe"
  role: "user"
}

Now, if I modify the “alg” in the header, set it to “None” and update the role in the payload

jsx
header: {
  alg: "none",
  typ: "JWT"
},
payload: {
	sub: "joe"
  role: "admin"
}
header: {
  alg: "none",
  typ: "JWT"
},
payload: {
	sub: "joe"
  role: "admin"
}

The new token is:

jsx
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.

If the verification function in our service checks the "alg" claim in a JWS's header to determine the algorithm used to sign the token and finds that "alg" is set to "None," it indicates that no verification algorithm is required. Consequently, the verification step will always succeed, as it does not actually perform any signature checking.

✅ For this reason, many libraries today report "alg": "none" tokens as invalid, even though there’s no signature in place

This attack is similar to the unsecured JWT attack and also relies on ambiguity in the API of certain JWT libraries. This a cryptographic vulnerability that arises when a system mistakenly uses a public key intended for RS256 (RSA signature) as the secret key for HS256 (HMAC with SHA-256).

RS256 relies on asymmetric cryptography, where a private key is used for signing JWTs, and the corresponding public key is used for verification. On the other hand, HS256 relies on symmetric cryptography, where a single secret key is shared between the token issuer and verifier for both signing and verification.

The attacker can modify the token to change the “alg” from RS256 to HS256. By doing this, when verifying the token, the recipient will inadvertently use the public key intended for RS256 as the secret key for HS256. As a result, an attacker can craft a JWT with a valid signature generated using the public key, allowing them to impersonate legitimate users or access unauthorized resources.

✅ To mitigate against this attack, we can hardcode the algorithm in the jwt verification function instead of relying on the alg claims from the token.

HMAC algorithms rely on a shared secret to produce and verify signatures. If we use this algorithm as the signing algorithm but using a weak symmetric key. The attacker can use offline brute-force or dictionary attacks to get the secret key and produce the valid JWT token.

✅ Mitigation against this attack might be not choosing human-memorizable passwords as the key to a keyed-MAC algorithm such as "HS256"

This attack comes from the incorrect validation assumptions. Sometimes, we only validate the validity of the token against the signature, or some claims in the payload. This can lead to the fact that, the attacker can re-use the valid JWT token from serviceA to make request to serviceB even though that JWT should not be authorized in serviceB.

✅ Always check for all the claims in the payload, token validation must rely on either unique, per-service keys or secrets, or specific claims. For instance, this token could include an aud claim specifying the intended audience. This way, even if the signature is valid, the token cannot be used on other services that share the same secret or signing key.

  • Always perform algorithm verification
  • Use appropriate algorithm
    • The JSON Web Algorithms spec provides recommended and required algorithms, but selecting the right one depends on the scenario. For instance, an HMAC-signed JWT might suffice for a single-server web app, but a shared secret algorithm would be cumbersome for federated identity. Ultimately, JWTs are invalid unless the validation algorithm aligns with the application's needs, emphasizing the importance of algorithm verification.
  • Always perform validation on all claims
  • Pick strong keys
    • This recommendation is often overlooked, even though it applies to any cryptographic key. For example, the minimum required length for HMAC shared secrets is frequently ignored. Additionally, even if the shared secret is long enough, it must also be fully random to resist brute-force attacks. To ensure randomness, key generating libraries should use cryptographic-quality pseudo-random number generators (PRNGs) properly seeded during initialization, or preferably, a hardware number generator. This advice applies to both shared-key and public-key algorithms. Furthermore, human-readable passwords are not considered secure enough for shared key algorithms, as they are vulnerable to dictionary attacks.

RFC 7519 - JSON Web Token (JWT) (ietf.org)

RFC 7515 - JSON Web Signature (JWS) (ietf.org)

RFC 7516 - JSON Web Encryption (JWE) (ietf.org)

JSON Web Token Introduction - jwt.io

Auth0 | JWT Handbook

Tagged:#Web Development
0