© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
D. R. HeffelfingerPayara Micro Revealedhttps://doi.org/10.1007/978-1-4842-8161-1_11

11. Security with JSON Web Tokens

David R. Heffelfinger1  
(1)
Fairfax, VA, USA
 

Traditionally, security in web applications has been implemented by keeping data about the logged-in user in the HTTP session. With microservices being inherently stateless, this approach is not suitable for a microservices architecture. To address security, MicroProfile has support for JSON Web Token (JWT).

JSON Web Tokens are a mechanism used to implement stateless security in microservices.

JWT is a JSON-based text format used for exchanging information between systems. JWT is an open standard, specified under RFC 7519. A JWT’s information is encapsulated in claims, which are essentially key value pairs. MicroProfile requires JWTs to be digitally signed using the RSA-SHA256 algorithm.

JWTs can contain arbitrary claims, all of them optional; the MicroProfile JWT specification, though, requires specific claims.

JWT Claim

Description

iss

Token issuer

at

The time the token was issued, it is specified as a long value representing the number of seconds between January 1, 1970 UTC and the issued time

exp

The time the token will expire, it is specified as a long value representing the number of seconds between January 1, 1970 UTC and the expiration time

upn

The username of the logged-in principal

In addition to the required claims, the MicroProfile JWT specification recommends the following claims to be present in a JWT:

JWT Claim

Description

jti

Unique identifier for the JWT

aud

Identifies the recipients that the JWT is intended for, interpretation of audience values is application specific

groups

Any security groups (AKA roles) that the user belongs to

Obtaining a Token

A JWT is typically obtained from some sort of identity server; when deploying to the cloud, most cloud providers provide a way to obtain a JWT to use with their servers. If deploying applications in-house, a JWT is typically obtained by an identity server tool deployed in-house, for example, something like the open source Keycloak tool or through commercial identity servers such as Okta or similar.

For testing and learning purposes, these tools are a bit overkill; thankfully, there is an open source tool developed by the prolific Java consultant Adam Bien, the tool is called jwtenizr, we can use it to quickly and easily generate a JWT for testing purposes.

jwtenizr can be downloaded from http://jwtenizr.sh/; it is a self-contained, executable JAR file; we can generate a token by issuing the following command from the command line:
java -jar jwtenizr.jar
The preceding command will generate output similar to the following:
Enable verbose output with java -Dverbose -jar jwtenizr.jar [optional: an URI for the generated curl]
The generated token token.jwt contains information loaded from: jwt-token.json
Adjust the groups[] to configure roles and upn to change the principal in jwt-token.json then re-execute JWTenizr
The iss in jwt-token.json has to correspond with the mp.jwt.verify.issuer in microprofile-config.properties
Copy the microprofile-config.properties to your WAR/src/main/resources/META-INF
Use the following command for testing:
curl -i -H'Authorization: Bearer eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkdWtlIiwidXBuIjoiZHVrZSIsImF1dGhfdGltZSI6MTYzOTQwODk4NywiaXNzIjoiYWlyaGFja3MiLCJncm91cHMiOlsiY2hpZWYiLCJoYWNrZXIiXSwiZXhwIjoxNjM5NDA5OTg3LCJpYXQiOjE2Mzk0MDg5ODcsImp0aSI6IjQyIn0.GnSwzNSO7EfKRD-ANqYhaVQuY8HIRdp29r-8K3lqpjCb2bByRmkdqTl9HPW4ePfqe0K7wpr_1Nfir12CPSb9e4i1PWw-qZxf9pPpEIeLRVB-_8p_n6FaTCkt4uSB12fcBMaLh2PZGOAnK5uWjAY1V-noD-KxmX_BsZnhSOssMa-0FnmevXlYPcRcbauVUxlTOACzFwdxpcRSq_Ms9QMh_5TctQZtq59VTnpOnfOZBIHd3eS0zB6AHqqNNtioPW8syX6iZxweSQkBX0pmYBaQxC2iUExhUOTmzEnDeBjaJ5EcdvAZ0-98w2GIqnZrftBlQvtWt25MKmNIl_NAtmBKZQ' http://localhost:8080
[10:23:07:548]JWT generated

Notice that the output of the tool includes example usage on how to invoke secured services using curl, very handy when testing our services.

jwtenizr generates a number of files:
  • jwtenizr-config.json: A JSON file we can use to configure jwtenizr, can be used to customize the JWT issuer and the location of the generated sample MicroProfile configuration file; it will be read in subsequent executions of jwtenizr.

  • jwt-token.json: Cleartext version of the generated JWT.

  • microprofile-config.properties: Sample microprofile-config.properties we can use to configure our secured applications.

  • token.jwt: Base 64 encoded version of the generated JWT.

Although JWT is fairly configurable, its defaults will suffice for our purposes. By default, jwtenizr generates a token for a user called “duke”, belonging to groups “chief” and “hacker”, issued by “airhacks”, and expiring 16 minutes and 40 seconds (1,000 seconds) from the time the token was created.

Securing Microservices with JSON Web Tokens

Securing our RESTful web services is primarily done via a few simple MicroProfile Config properties and some annotations.

MicroProfile Config JWT Properties

As previously mentioned, JSON Web Tokens are digitally signed; this is done by encrypting the token with the issuer’s private key. To verify the validity of the token, we need to decrypt it with the issuer’s public key.

MicroProfile JWT provides a standard configuration property we can use to specify the public key; the name of the property is mp.jwt.verify.publickey; its value must be the Base 64 encoded public key; this value is typically provided by whichever identity service we may be using. In our example, the value appears in the sample MicroProfile configuration file generated by jwtenizr.

Alternatively, we could put the public key in a text file and either publish that text file in a URL or place it inside the JAR file; in a typical Maven project, the file containing the public key would be placed under src/main/resources or one of its subdirectories.

If, for example, we placed the key under src/main/resources/keys/publicKey.pem, the value of the mp.jwt.verify.publickey property would be /keys/publicKey.pem; if, instead, we placed the public key directly in src/main/resources, then the corresponding property value would be /publicKey.pem.

In addition to specifying the public key, we need to specify the issuer of the token; this is specified in the iss claim of the JWT we are using and can be obtained from the cleartext JSON Web Token we are using to authenticate. jwtenizr uses airhacks as its default issuer; therefore, we must configure our application accordingly. jwtenizr conveniently generates a microprofile-config.properties we can use; it should look similar to the following:
#generated by jwtenizr
#Mon Dec 13 18:05:17 EST 2021
mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArOmNhZNAb2UXrMQ+TOp4hOCX4/QN1lC7DJW9Sw8PRZF85SrxKe2rAt8u0aaPg8KZdYsmAfxJU7ZsILHFV7cT9ixYyZcIz556CpluhQmJiVlBEDi6lX9IIbhXsfeaCXPATd+0fe0Yg4CLtfGeJjZQQOK8yabqsRemhQ84s/alCfWeWm7zp0DHe0PH+2kNkLVeSg4cigAzakDiW9JYNs5+7XzqPujYpNOjJqCltDfPkz0c0bqIOMKr7cm0G+WTQIqXxI46y1vUTYCdH+irJuJF8FPlL84Rd1NYrRtCLslFhqfLhSELYRWu7lyXsH89QSAjFTmY1ofzmWdawPYkIIQJrwIDAQAB
mp.jwt.verify.issuer=airhacks

Placing the file under src/main/resources/META-INF in our Maven project will take care of our JWT configuration needs.

MicroProfile JWT Annotations

Every JAX-RS application requires an instance of a class that extends javax.ws.rs.core.Application; typically, we annotate this class with @ApplicationPath to specify the base URI for all resource URIs in our application. When securing our applications, we need to annotate this class with the @LoginConfig annotation as illustrated in the following example.
package com.ensode.jwtdemo;
import javax.inject.Singleton;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import org.eclipse.microprofile.auth.LoginConfig;
@Singleton
@ApplicationPath("webresources")
@LoginConfig(authMethod = "MP-JWT")
public class ApplicationConfig extends Application {
}

As seen in the example, we need to set the authMethod attribute of @LoginConfig to "MP-JWT"; this tells the MicroProfile runtime (Payara Micro) that our application is using JWT for authentication.

Additionally, our application class needs to be turned into a singleton by annotating it with javax.inject.Singleton; failure to do this will prevent clients from being authenticated properly.

Once we have specified we are using JWT for authentication, we need to specify which roles can access our protected endpoints; this is done via the @RolesAllowed annotation, as illustrated in the following example:
package com.ensode.jwtdemo;
import javax.annotation.security.RolesAllowed;
//additional imports omitted
@RequestScoped
@Path("jwtdemo")
public class JwtDemoResource {
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  @RolesAllowed({"chief"})
  public String secured() {
    return "Secured endpoint accessed successfully ";
  }
}

The @RolesAllowed annotation can be used at the class level, in which case it will apply to all endpoints in the class, or at the method level, in which case it will only apply to the annotated method; its value must be an array of strings containing valid roles that are allowed to access the endpoint; these roles are taken from the groups claim in the JWT. In our example, we used jwtenizr to generate our token, which, by default, creates a token with the following groups/roles: “chief” and “hacker”; in our example, only users with the role of “chief” will be able to access the endpoint.

In order for the @RolesAllowed annotation to work as expected, we need to turn our JAX-RS class into a CDI request scoped bean via the @RequestScoped annotation, as illustrated in the example.

We can test our secured endpoint with curl by sending the Base 64 encoded token as an HTTP header, for example:
curl -H'Authorization: Bearer eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkdWtlIiwidXBuIjoiZHVrZSIsImF1dGhfdGltZSI6MTYzOTQzOTIxNCwiaXNzIjoiYWlyaGFja3MiLCJncm91cHMiOlsiY2hpZWYiLCJoYWNrZXIiXSwiZXhwIjoxNjM5NDQwMjE0LCJpYXQiOjE2Mzk0MzkyMTQsImp0aSI6IjQyIn0.gMwd042R8L_pxmj97j9Bzsz8Vg5cZCSZ_uiSJwqnBD5i811Wq_4l9_DQ-LdRGQwgORKrN3emVfkxOiiWnTSSfEIOKDktuCiJuhlcojeaTvpACaFtnYLI5fZ6hCkurj7TEnrJwVqxJVo_dH6AS45UR2Q3z688oBBSb5_i5gtw9g_84nkWp70FkAjDusz-jLWeebOWAEk6NRv6fN_ZAP1JJcz9WoNordMeXgVe2tFmLz8_yDY_ba-0x1F9vEXBalHz4AwJaAYtW97mvshdnLnKkP48mQGoO4X9Ps6eRCG1viTuTmcHRTACt9osb7wssWIDenhuumz_lI9P9sLVFfj29Q' http://localhost:8080/jwt-demo/webresources/jwtdemo

The preceding curl command will result in a successful invocation to our endpoint; if we remove the -H option from the command, then the request will fail with an HTTP status code of 401: Unauthorized.

To authorize requests via the MicroProfile REST client API, we need to send the Base 64 encoded token as well; we will cover how to do this in the next section.

Invoking Secured Microservices with MicroProfile REST Client API

In order to invoke a secured RESTful web service via the MicroProfile REST client API, we need to write an interface, and it needs to be annotated with @RegisterRestClient as usual; however, in this case, there are some slight differences in the method signature and annotations used in the client interface, compared to the code implementing the endpoint.
package com.ensode.jwtclientdemo;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.core.HttpHeaders;
//other imports omitted
@RegisterRestClient
@Path("jwtdemo")
public interface JwtDemoResourceClient {
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String secured(@HeaderParam(HttpHeaders.AUTHORIZATION)
    String authorizationHeader);
}

The first thing to note is that we should not use the @RolesAllowed annotation on the client interface; this would result in the client itself being secured, which is not what we want in our example; what we are aiming to do here is to invoke a secured endpoint from an unsecured one; in order to do that, we need to send the JWT token as an HTTP header.

We can send an HTTP header in by annotating a String parameter with the @HeaderParam annotation; the value attribute of this annotation specifies the header name; since “Authorization” is a standard HTTP header name, it is defined as the AUTHORIZATION constant in the HttpHeaders class.

Notice that the method signature does not exactly match the corresponding signature in the JAX-RS service code; the endpoint does not define the authorizationHeader parameter.

We use the REST client interface as usual by injecting it via the @Inject and @RestClient annotations and then passing the JWT token as a parameter that will be processed as an HTTP header, thanks to the @HeaderParam interface.
package com.ensode.jwtclientdemo;
//imports omitted
@ApplicationScoped
@Path("jwtclient")
public class JwtClientResource {
  @Inject
  @RestClient
  private JwtDemoResourceClient jwtDemoResourceClient;
  @Inject
  @ConfigProperty(name = "ensode.jwt.header.string")
  private String jwtHeaderString;
  @GET
  public String accessSecuredEndpoint() {
    jwtDemoResourceClient.secured(
      "Bearer ".concat(jwtHeaderString));
    return "secured endpoint accessed successfully";
  }
}

We can obtain the JWT token as a simple string; in our example, we are setting it up as a MicroProfile Config property and retrieving it from there; we then invoke the client method, passing the JWT token as a parameter; in order for authentication to work as expected, we need to prefix the token text with the word “Bearer”, which we are doing as we invoke the method on the client interface.

For completeness and clarity, here is the property file we used to store the JWT text:
com.ensode.jwtclientdemo.JwtDemoResourceClient/mp-rest/url=http://localhost:8080/jwt-demo/webresources
ensode.jwt.header.string=eyJraWQiOiJqd3Qua2V5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkdWtlIiwidXBuIjoiZHVrZSIsImF1dGhfdGltZSI6MTYzOTQ4ODc4OCwiaXNzIjoiYWlyaGFja3MiLCJncm91cHMiOlsiY2hpZWYiLCJoYWNrZXIiXSwiZXhwIjoxNjM5NDg5Nzg4LCJpYXQiOjE2Mzk0ODg3ODgsImp0aSI6IjQyIn0.C9_ejgLALOo8IVHZ6P6fqoTrVIjrmaqJ_gexHrEzwx0jWExDwLmKLIWKcOHWytKPrW0k1ok8cKUj55rKWG8Vp1-gGhIptWeGF2ckSKu5fCOaoBerJn6x8uDJVeURV8-ABHcoJFSePQx3EEGmoLEb2_EqgBvU4didp-MyUwyyIKiDIuOSu_n-mFq_yB9WpZZiY4lyNS9ewt_TDUmIgteJ2VMsmZaHp3n_RygV0S4FPXhOUHRhKOfLLUorvJzGYsT7dgjbnY6kyh4dgAXWrGnX4kaf0ZFyVXCxsmuvks9Ixltv6krqWe_j-0ZkYMzevgnXbvc3MA9LJWQMhauwtGfw1g

Notice how the JWT token text is specified as the value of the ensode.jwt.header.string property, which we retrieve in our code via the @ConfigProperty annotation and pass to the client interface.

Obtaining Information from a Token

We can inject an instance of org.eclipse.microprofile.jwt.JsonWebToken into our JAX-RS resource; this class has a number of getter methods we can use to extract information from the token that was used to access the page.
package com.ensode.jwtdemo;
//additional imports omitted
import org.eclipse.microprofile.jwt.JsonWebToken;
@RequestScoped
@Path("jwtdemo")
public class JwtDemoResource {
  private static final Logger LOGGER = Logger.getLogger(JwtDemoResource.class.getName());
  @Inject
  private JsonWebToken jsonWebToken;
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  @RolesAllowed({"chief"})
  public String secured() {
    LOGGER.log(Level.INFO, String.format("Audience: %s",
      jsonWebToken.getAudience()));
    LOGGER.log(Level.INFO, String.format("Expiration Time: %s",
      jsonWebToken.getExpirationTime()));
    LOGGER.log(Level.INFO, String.format("Groups: %s",
      jsonWebToken.getGroups()));
    LOGGER.log(Level.INFO, String.format("Issued at time: %s",
      jsonWebToken.getIssuedAtTime()));
    LOGGER.log(Level.INFO, String.format("Issuer: %s",
      jsonWebToken.getIssuer()));
    LOGGER.log(Level.INFO, String.format("Name: %s",
      jsonWebToken.getName()));
    LOGGER.log(Level.INFO, String.format("Raw Token: %s",
      jsonWebToken.getRawToken()));
    LOGGER.log(Level.INFO, String.format("Subject: %s",
     jsonWebToken.getSubject()));
    LOGGER.log(Level.INFO, String.format("Token ID: %s",
      jsonWebToken.getTokenID()));
    return "Secured endpoint accessed successfully ";
  }
}
The following table lists all the getter methods we can use to extract information from the token, along with the corresponding claim where the information is extracted from.

Getter Method

JWT Claim

Description

getAudience()

aud

Intended audience

getExpirationTime()

exp

Expiration time, as the number of seconds since January 1, 1970 UTC

getGroups()

groups

The groups (or roles) the user belongs to

getIssuedAtTime()

iat

The time the token was issued, expiration time, as the number of seconds since January 1, 1970 UTC

getIssuer()

iss

Organization that issued the token

getName()

upn

Unique Principal Name (the username)

getRawToken()

N/A

Base 64 encoded string representing the token (the value that we pass as the HTTP authorization header)

getSubject()

sub

Additional identifier for the user could be used to store the user’s full name (i.e., “John Doe”)

getTokenId()

jti

JSON Token ID

All of the preceding getter methods are for claims that the MicroProfile JWT specification either requires or recommends; if any of the recommended claims is not present, the getter will return null. A JWT token, though, may have an arbitrary number of claims, many of which we don’t have a getter method for.

If we know in advance the name of a claim whose value we would like to retrieve, we can do so by invoking the claim() method on an instance of JsonWebToken; for example, if we know our token has a claim named “foo”, we can obtain its value by invoking jsonWebToken.claim("foo").

There may be times when we don’t know in advance all the claim names in our token; to handle this situation, the JsonWebToken class has a claimNames() method that returns a set of strings containing all the claim names in our token; we can use this set to obtain the values of all claims in our token; the following example illustrates how to do this:
package com.ensode.jwtdemo;
//imports omitted
@RequestScoped
@Path("jwtdemo")
public class JwtDemoResource {
  private static final Logger LOGGER = Logger.getLogger(JwtDemoResource.class.getName());
  @Inject
  private JsonWebToken jsonWebToken;
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  @RolesAllowed({"chief"})
  public String secured() {
    Set<String> claimNames = jsonWebToken.getClaimNames();
    LOGGER.log(Level.INFO, "--- Begin Token Claims");
    claimNames.forEach(claimName -> LOGGER.log(Level.INFO,
            String.format("%s: %s", claimName,
                    jsonWebToken.claim(claimName).orElse(""))));
    LOGGER.log(Level.INFO, "--- End Token Claims");
    return "Secured endpoint accessed successfully ";
  }
}

In this example, we obtain a Set of String containing all the claim names, then traverse the set and extract the value of each claim, and then send the output to the Payara Micro output. Recall that JsonWebToken.claim() returns an Optional; in our example, if the Optional happens to be empty, use an empty String ("") as the corresponding value.

Summary

In this chapter, we covered how to secure our RESTful web services with JSON Web Tokens.

We covered how to obtain a token so that we can use it to secure our applications; we then explained how to configure our RESTful web services so that they require authentication.

Additionally, we covered how to securely invoke a secured RESTful web service.

Finally, we covered how to extract information from a JSON Web Token in our code.

..................Content has been hidden....................

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