Appendix B. JSON Web Token

We’ve discussed JSON Web Token (JWT) many times in this book. In chapter 2, we talked about how we can use a JWT as an OAuth 2.0 self-contained access token, and in chapter 4, we described how OpenID Connect uses a JWT as its ID token to transfer user claims from the OpenID provider to the client application. In chapter 7, we discussed how to pass end-user context in a JWT among services in a microservices deployment. In chapter 11, we examined how each pod in Kubernetes uses a JWT to authenticate to the Kubernetes API server. In chapter 12, we showed how an Istio service mesh uses JWT to verify the end-user context at the Envoy proxy. Finally, in appendix F, we described how an Open Policy Agent (OPA) uses JWT to carry policy data along with the authorization request.

All in all, JWT is an essential ingredient in securing a microservices deployment. In this appendix, we discuss JWT in detail. If you are interested in understanding further internals of JWT, we recommend Advanced API Security: OAuth 2.0 and Beyond (Apress, 2019) by Prabath Siriwardena (a coauthor of this book), and the YouTube video JWT Internals and Applications (www.youtube.com/watch?v= c-jsKk1OR24), presented by Prabath Siriwardena.

B.1 What is a JSON Web Token?

A JWT (pronounced jot) is a container that carries different types of assertions or claims from one place to another in a cryptographically safe manner. An assertion is a strong statement about someone or something issued by an entity. This entity is also known as the issuer of the assertion.

Imagine that your state’s Department of Motor Vehicles (DMV) can create a JWT (to represent your driver’s license) with your personal information, which includes your name, address, eye color, hair color, gender, date of birth, license expiration date, and license number. All these items are attributes, or claims, about you and are also known as attribute assertions. The DMV is the issuer of the JWT.

Anyone who gets this JWT can decide whether to accept what’s in it as true, based on the level of trust they have in the issuer of the token (in this case, the DMV). But before accepting a JWT, how do you know who issued it? The issuer of a JWT signs it by using the issuer’s private key. In the scenario illustrated in figure B.1, a bartender, who is the recipient of the JWT, can verify the signature of the JWT and see who signed it.


Figure B.1 A JWT is used as a container to transport assertions from one place to another in a cryptographically safe manner. The bartender, who is the recipient of the JWT, accepts the JWT only if they trust the DMV, the issuer of the JWT.

In addition to attribute assertions, a JWT can carry authentication and authorization assertions. In fact, a JWT is a container; you can fill it with anything you need. An authentication assertion might be the username of the user and how the issuer authenticates the user before issuing the assertion. In the DMV use case, an authentication assertion might be your first name and last name (or even your driver’s license number), or how you are known to the DMV.

An authorization assertion is about the user’s entitlements, or what the user can do. Based on the assertions the JWT brings from the issuer, the recipient can decide how to act. In the DMV example, if the DMV decides to embed the user’s age as an attribute in the JWT, that data is an attribute assertion, and a bartender can do the math to calculate whether the user is old enough to buy a beer. Also, without sharing the user’s age with a bartender, the DMV may decide to include an authorization assertion stating that the user is old enough to buy a beer. In that case, a bartender will accept the JWT and let the user buy a beer. The bartender wouldn’t know the user’s age, but the DMV authorized the user to buy beer.

In addition to carrying a set of assertions about the user, a JWT plays another role behind the scenes. Apart from the end user’s identity, a JWT also carries the issuer’s identity, which is the DMV in this case. The issuer’s identity is implicitly embedded in the signature of the JWT. By looking at the corresponding public key while validating the signature of the token, the recipient can figure out who the token issuer is.

B.2 What does a JWT look like?

Before we delve deep into the JWT use cases within a microservices deployment, take a closer look at a JWT. Figure B.2 shows the most common form of a JWT. This figure may look like gibberish unless your brain is trained to decode base64url-encoded strings.


Figure B.2 A base64url-encoded JWT, which is also a JWS

What you see in figure B.2 is a JSON Web Signature (JWS), which we discuss in detail in section B.3. The JWS, which is the most commonly used format of a JWT, has three parts, with a dot (.) separating each part:

  • The first part is known as the JSON Object Signing and Encryption (JOSE) header.

  • The second part is the claims set, or body (or payload).

  • The third part is the signature.

The JOSE header is a base64url-encoded JSON object, which expresses the metadata related to the JWT, such as the algorithm used to sign the message. Here’s the base64url-decoded JOSE header:

{
  "alg": "RS256",
}

The JWT claims set is a base64url-encoded JSON object, which carries the assertions (between the first and second separators). Following is the base64url-decoded claims set:

{
  "sub": "peter",
  "aud": "*.ecomm.com",
  "nbf": 1533270794,
  "iss": "sts.ecomm.com",
  "exp": 1533271394,
  "iat": 1533270794,
  "jti": "5c4d1fa1-7412-4fb1-b888-9bc77e67ff2a"
}

The JWT specification (RFC 7519) defines seven attributes: sub, aud, nbf, iss, exp, iat, and jti. None of these are mandatory--and it’s up to the other specifications that rely on JWT to define what is mandatory and what is optional. For example, the OpenID Connect specification makes the iss attribute mandatory. These seven attributes that the JWT specification defines are registered in the Internet Assigned Numbers Authority (IANA) Web Token Claims registry. However, you can introduce your own custom attributes to the JWT claims set. In the following sections, we discuss these seven attributes in detail.

B.2.1 The issuer of a JWT

The iss attribute in the JWT claims set carries an identifier corresponding to the issuer, or asserting party, of the JWT. The JWT is signed by the issuer’s private key. In a typical microservices deployment within a given trust domain, all the microservices trust a single issuer, and this issuer is typically known as the security token service (STS).

B.2.2 The subject of a JWT

The sub attribute in the JWT claims set defines the subject of a JWT. The subject is the owner of the JWT--or in other words, the JWT carries the claims about the subject. The applications of the JWT can further refine the definition of the sub attribute. For example, the OpenID Connect specification makes the sub attribute mandatory, and the issuer of the token must make sure that the sub attribute carries a unique identifier.

B.2.3 The audience of a JWT

The aud attribute in the JWT claims set specifies the audience, or intended recipient, of the token. In figure B.2, it’s set to the string value *.ecomm.com. The value of the aud attribute can be any string or a URI that’s known to the microservice or the recipient of the JWT.

Each microservice must check the value of the aud parameter to see whether it’s known before accepting any JWT as valid. If you have a microservice called foo with the audience value foo.ecomm.com, the microservice should reject any JWT carrying the aud value bar.ecomm.com, for example. The logic in accepting or rejecting a JWT based on audience is up to the corresponding microservice and to the overall microservices security design. By design, you can define a policy to agree that any microservice will accept a token with the audience value <microservice identifier>.ecomm .com or *.ecomm.com, for example.

B.2.4 JWT expiration, not before and issued time

The value of the exp attribute in the JWT claims set expresses the time of expiration in seconds, which is calculated from 1970-01-01T0:0:0Z as measured in Coordinated Universal Time (UTC). Any recipient of a JWT must make sure that the time represented by the exp attribute is not in the past when accepting a JWT--or in other words, the token is not expired. The iat attribute in the JWT claims set expresses the time when the JWT was issued. That too is expressed in seconds and calculated from 1970-01-01T0:0:0Z as measured in UTC.

The time difference between iat and exp in seconds isn’t the lifetime of the JWT when there’s an nbf (not before) attribute present in the claims set. You shouldn’t start processing a JWT (or accept it as a valid token) before the time specified in the nbf attribute. The value of nbf is also expressed in seconds and calculated from 1970-01-01T0:0:0Z as measured in UTC. When the nbf attribute is present in the claims set, the lifetime of a JWT is calculated as the difference between the exp and nbf attributes. However, in most cases, the value of nbf is equal to the value of iat.

B.2.5 The JWT identifier

The jti attribute in the JWT claims set defines a unique identifier for the token. Ideally, the token issuer should not issue two JWTs with the same jti. However, if the recipient of the JWT accepts tokens from multiple issuers, a given jti will be unique only along with the corresponding issuer identifier.

B.3 JSON Web Signature

The JWT explained in section B.2 (and, as a reminder, shown in figure B.3) is also a JSON Web Signature. JWS is a way to represent a signed message. This message can be anything, such as a JSON payload, an XML payload, or a binary.


Figure B.3 Base64url-encoded JWT, which is also a JWS

A JWS can be serialized in two formats, or represented in two ways: compact serialization and JSON serialization. We don’t call every JWS a JWT. A JWS becomes a JWT only when it follows compact serialization and carries a JSON ob-ject as the payload. Under JWT terminology, we call this payload the claims set. Figure B.4 shows a compact-serialized JWS--or a JWT. Section B.3 details the meaning of each component in figure B.4.


Figure B.4 A JWT that is a compact-serialized JWS with a JOSE header, a claims set, and a signature

With JSON serialization, the JWS is represented as a JSON payload (see figure B.5). It’s not called a JWT. The payload parameter in the JSON-serialized JWS can carry any value. The message being signed and represented in figure B.5 is a JSON message with all its related metadata.

Unlike in a JWT, a JSON serialized JWS can carry multiple signatures corresponding to the same payload. In figure B.5, the signatures JSON array carries two elements, and each element carries a different signature of the same payload. The protected and header attributes inside each element of the signatures JSON array define the metadata related to the corresponding signature.


Figure B.5 A JWS with JSON serialization that includes related metadata

Let’s see how to use the open source Nimbus (https://connect2id.com/products/nimbus-jose-jwt) Java library to create a JWS. The source code related to all the samples used in this appendix is available in the https://github.com/microservices-security-in-action/samples GitHub repository, inside the appendix-b directory.

NOTE Before running the samples in this appendix, make sure that you have downloaded and installed all the required software as mentioned in section 2.1.1.

Let’s build the sample, which builds the JWS, and run it. Run the following Maven command from the appendix-b/sample01 directory. It may take a couple of minutes to finish the build process when you run this command for the first time. If everything goes well, you should see the BUILD SUCCESS message at the end:

> mvn clean install
[INFO] BUILD SUCCESS

Now run your Java program to create a JWS with the following command (from the appendix-b/sample01/lib directory). If it executes successfully, it prints the base64url-encoded JWS:

> java  -cp "../target/com.manning.mss.appendixb.sample01-1.0.0.jar:*" 
com.manning.mss.appendixb.sample01.RSASHA256JWTBuilder

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJwZXRlciIsImF1ZCI6IiouZWNvbW0uY29tIiwibmJmIj
oxNTMzMjcwNzk0LCJpc3MiOiJzdHMuZWNvbW0uY29tIiwiZXhwIjoxNTMzMjcxMzk0LCJpYXQiO
jE1MzMyNzA3OTQsImp0aSI6IjVjNGQxZmExLTc0MTItNGZiMS1iODg4LTliYzc3ZTY3ZmYyYSJ9
.aOkwoXAsJHz1oD-N0Zz4-dvZBtz7oaBXyoysfTKy2vV6C_Sfw05w10Yg0oyQX6VBK8tw68Tair
pA9322ZziTcteGxaNb-Hqn39krHT35sD68sNOkh7zIqLIIJ59hisO81kK11g05Nr-nZnEv9mfHF
vU_dpQEP-Dgswy_lJ8rZTc

You can decode this JWS by using the JWT decoder available at https://jwt.io. The following is the decoded JWS claims set, or payload:

{
  "sub": "peter",
  "aud": "*.ecomm.com",
  "nbf": 1533270794,
  "iss": "sts.ecomm.com",
  "exp": 1533271394,
  "iat": 1533270794,
  "jti": "5c4d1fa1-7412-4fb1-b888-9bc77e67ff2a"
}

NOTE If you get any errors while executing the previous command, check whether you executed the command from the correct location. It has to be from inside the appendix-b/sample01/lib directory, not from the appendix-b/ sample01 directory. Also make sure that the value of the -cp argument is within double quotes.

Take a look at the code that generated the JWT. It’s straightforward and self-explanatory with comments. You can find the complete source code in the sample01/src/main/java/com/manning/mss/appendixb/sample01/RSASHA256JWTBuilder.java file.

The following method does the core work of JWT generation. It accepts the token issuer’s private key as an input parameter and uses it to sign the JWT with RSA-SHA256.

Listing B.1 The RSASHA256JWTBuilder.java file

public static String buildRsaSha256SignedJWT(PrivateKey privateKey)
                                             throws JOSEException {

  // build audience restriction list.
  List<String> aud = new ArrayList<String>();
  aud.add("*.ecomm.com");

  Date currentTime = new Date();

  // create a claims set.
  JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder().
  // set the value of the issuer.
  issuer("sts.ecomm.com").
  // set the subject value - JWT belongs to this subject.
  subject("peter").
  // set values for audience restriction.
  audience(aud).
  // expiration time set to 10 minutes.
  expirationTime(new Date(new Date().getTime() + 1000 * 60 * 10)).
  // set the valid from time to current time.
  notBeforeTime(currentTime).
  // set issued time to current time.
  issueTime(currentTime).
  // set a generated UUID as the JWT identifier.
  jwtID(UUID.randomUUID().toString()).build();
  // create JWS header with RSA-SHA256 algorithm.

  JWSHeader jswHeader = new JWSHeader(JWSAlgorithm.RS256);

  // create signer with the RSA private key..
  JWSSigner signer = new RSASSASigner((RSAPrivateKey) privateKey);

  // create the signed JWT with the JWS header and the JWT body.
  SignedJWT signedJWT = new SignedJWT(jswHeader, jwtClaims);

  // sign the JWT with HMAC-SHA256.
  signedJWT.sign(signer);

  // serialize into base64url-encoded text.
  String jwtInText = signedJWT.serialize();

  // print the value of the JWT.
  System.out.println(jwtInText);

  return jwtInText;
}

B.4 JSON Web Encryption

In the preceding section, we stated that a JWT is a compact-serialized JWS. It’s also a compact-serialized JSON Web Encryption (JWE). Like JWS, a JWE represents an encrypted message using compact serialization or JSON serialization. A JWE is called a JWT only when compact serialization is used. In other words, a JWT can be either a JWS or a JWE, which is compact serialized. JWS addresses the integrity and nonrepudiation aspects of the data contained in it, while JWE protects the data for confidentiality.

A compact-serialized JWE (see figure B.6) has five parts; each part is base64url-encoded and separated by a dot (.). The JOSE header is the part of the JWE that carries metadata related to the encryption. The JWE encrypted key, initialization vector, and authentication tag are related to the cryptographic operations performed during the encryption. We won’t talk about those in detail here. If you’re interested, we recommend the blog “JWT, JWS, and JWE for Not So Dummies” at http://mng.bz/gya8. Finally, the ciphertext part of the JWE includes the encrypted text.


Figure B.6 A JWT that’s a compact-serialized JWE

With JSON serialization, the JWE is represented as a JSON payload. It isn’t called a JWT. The ciphertext attribute in the JSON-serialized JWE carries the encrypted value of any payload, which can be JSON, XML or even binary. The actual payload is encrypted and represented in figure B.7 as a JSON message with all related metadata.


Figure B.7 A JWE with JSON serialization and all related metadata

Let’s see how to use the open source Nimbus Java library to create a JWE. The source code related to all the samples used in this appendix is available in the https://github.com/microservices-security-in-action/samples Git repository inside the appendix-b directory. Before you delve into the Java code that you’ll use to build the JWE, try to build the sample and run it. Run the following Maven command from the appendix-b/sample02 directory. If everything goes well, you should see the BUILD SUCCESS message at the end:

> mvn clean install
[INFO] BUILD SUCCESS

Now run your Java program to create a JWE with the following command (from the appendix-b/sample02/lib directory). If it executes successfully, it prints the base64url-encoded JWE:

> java  -cp "../target/com.manning.mss.appendixb.sample02-1.0.0.jar:*" 
com.manning.mss.appendixb.sample02.RSAOAEPJWTBuilder

eyJlbmMiOiJBMTI4R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.Cd0KjNwSbq5OPxcJQ1ESValmRGPf
7BFUNpqZFfKTCd-9XAmVE-zOTsnv78SikTOK8fuwszHDnz2eONUahbg8eR9oxDi9kmXaHeKXyZ9
Kq4vhg7WJPJXSUonwGxcibgECJySEJxZaTmA1E_8pUaiU6k5UHvxPUDtE0pnN5XD82cs.0b4jWQ
HFbBaM_azM.XmwvMBzrLcNW-oBhAfMozJlmESfG6o96WT958BOyfjpGmmbdJdIjirjCBTUATdOP
kLg6-YmPsitaFm7pFAUdsHkm4_KlZrE5HuP43VM0gBXSe-41dDDNs7D2nZ5QFpeoYH7zQNocCjy
bseJPFPYEw311nBRfjzNoDEzvKMsxhgCZNLTv-tpKh6mKIXXYxdxVoBcIXN90UUYi.mVLD4t-85
qcTiY8q3J-kmg

Following is the decrypted JWE payload:

JWE Header:{"enc":"A128GCM","alg":"RSA-OAEP"}
JWE Content Encryption Key: Cd0KjNwSbq5OPxcJQ1ESValmRGPf7BFUNpqZFfKTCd-9
XAmVE-zOTsnv78SikTOK8fuwszHDnz2eONUahbg8eR9oxDi9kmXaHeKXyZ9Kq4vhg7WJPJXS
UonwGxcibgECJySEJxZaTmA1E_8pUaiU6k5UHvxPUDtE0pnN5XD82cs
Initialization Vector: 0b4jWQHFbBaM_azM
Ciphertext: XmwvMBzrLcNW-oBhAfMozJlmESfG6o96WT958BOyfjpGmmbdJdIjirjCBTUA
TdOPkLg6-YmPsitaFm7pFAUdsHkm4_KlZrE5HuP43VM0gBXSe-41dDDNs7D2nZ5QFpeoYH7z
QNocCjybseJPFPYEw311nBRfjzNoDEzvKMsxhgCZNLTv-tpKh6mKIXXYxdxVoBcIXN90UUYi
Authentication Tag: mVLD4t-85qcTiY8q3J-kmg
Decrypted Payload: 
{
  "sub":"peter",
  "aud":"*.ecomm.com",
  "nbf":1533273878,
  "iss":"sts.ecomm.com",
  "exp":1533274478,
  "iat":1533273878,
  "jti":"17dc2461-d87a-42c9-9546-e42a23d1e4d5"
}

NOTE If you get any errors while executing the previous command, check whether you executed the command from the correct location. It has to be from inside the appendix-b/sample02/lib directory, not from the appendix-b/ sample02 directory. Also make sure that the value of the -cp argument is within double quotes.

Now take a look at the code that generated the JWE. It’s straightforward and self-explanatory with code comments. You can find the complete source code in the sample02/src/main/java/com/manning/mss/appendixb/sample02/RSAOAEPJWT Builder.java file. The method in the following listing does the core work of JWE encryption. It accepts the token recipient public key as an input parameter and uses it to encrypt the JWE with RSA-OAEP.

Listing B.2 The RSAOAEPJWTBuilder.java file

public static String buildEncryptedJWT(PublicKey publicKey)
                                       throws JOSEException {

  // build audience restriction list.
  List<String> aud = new ArrayList<String>();

  aud.add("*.ecomm.com");

  Date currentTime = new Date();

  // create a claims set.
  JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder().

  // set the value of the issuer.
  issuer("sts.ecomm.com").
  // set the subject value - JWT belongs to this subject.
  subject("peter").
  // set values for audience restriction.
  audience(aud).
  // expiration time set to 10 minutes.
  expirationTime(new Date(new Date().getTime() + 1000 * 60 * 10)).
  // set the valid from time to current time.
  notBeforeTime(currentTime).
  // set issued time to current time.
  issueTime(currentTime).
  // set a generated UUID as the JWT identifier.
  jwtID(UUID.randomUUID().toString()).build();
  // create JWE header with RSA-OAEP and AES/GCM.
  JWEHeader jweHeader = new JWEHeader(JWEAlgorithm.RSA_OAEP,
                                      EncryptionMethod.A128GCM);

  // create encrypter with the RSA public key.
  JWEEncrypter encrypter = new RSAEncrypter((RSAPublicKey) publicKey);

  // create the encrypted JWT with the JWE header and the JWT payload.
  EncryptedJWT encryptedJWT = new EncryptedJWT(jweHeader, jwtClaims);

  // encrypt the JWT.
  encryptedJWT.encrypt(encrypter);

  // serialize into base64url-encoded text.
  String jwtInText = encryptedJWT.serialize();

  // print the value of the JWT.
  System.out.println(jwtInText);

  return jwtInText;
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.129.19