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.
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 |
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.
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-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.
Placing the file under src/main/resources/META-INF in our Maven project will take care of our JWT configuration needs.
MicroProfile JWT Annotations
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.
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.
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
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 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.
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
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").
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.