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.
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.
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.
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.
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 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.
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).
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.
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.
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
.
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.
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.
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.
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.
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.
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; }
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.
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.
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.
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; }
18.221.129.19