Skip to main content

In the post, “What is Eclipse MicroProfile”, we explained what Eclipse MicroProfile is and why it’s important. MicroProfile is made up of several specifications. In this post, I’ll explain the JSON Web Tokens (JWT), the MicroProfile JWT specification, and how it can be used to implement stateless security in microservices. I’ll also talk about the extensibility and flexibility of MicroProfile with claims.

Tomitribe has been helping companies implement REST services for years and one of the most common problems our clients have is deciding how to implement authentication and authorization. The development of MicroProfile and its use of JWT is based, in part, on those years of experience and insights we’ve gained while developing Tribestream, our API Gateway for microservices.

What is JWT?

For a while, companies used to store user contextual information in HTTP Sessions. It has worked for years, but in a clustering architecture, it proved to be expensive, unreliable, and painful to scale. However, with microservices and REST, which are stateless, HTTP Session state is not used eliminating the problem of sharing session state. The question is: How and where to save security context? The answer is JWT.

JWT stands for JSON Web Token. It’s a JSON-based text format for exchanging information between parties. JWT is an open standard specified under RFC 7519. The information contained in the JWT is called claims and the JWT is usually digitally signed (i.e. JSON Web Signature) so that the information can be verified and trusted. Optionally, it’s also possible to encrypt the claims (i.e. JSON Web Encryption) so it’s not in clear text within the JWT.

JWT is widely used because it is simple and efficient. One of the ways it’s used is exchanging authentication and authorization information between parties so that a remote server can identify the caller, verify the caller’s identity by checking the signature, and implement Role Based Access Control based on roles included in the JWT call. Standards such as OpenID Connect and OAuth 2 use JWT to represent their own tokens.

A JWT is composed of:

  • Header: the header contains metadata such as the type of algorithm used to sign the token (HS256 for HMAC for instance, RS256 for RSA, ES256 for Elliptic Curves), the type of the token (OpenID Connect, OAuth2, Microprofile JWT), etc
  • Claims: the claims is basically all the information you want to store in the token. Some are required depending on the type of token.
  • Signature: the signature, although optional, is highly recommended. If you want to trust something coming from the outside, you need the signature. It allows you to know the content hasn’t been changed by the caller or in between (man in the middle). I don’t recommend using MicroProfile JWT without a digital signature.

In its serialized version, a JWT token looks like:

base64(header).base64(claims).base64(signature)

As an example:

eyJraWQiOiJteS1yc2Eta2V5IiwiY3R5IjoianNvbiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU
  2In0.eyJzdWIiOiJhbGV4IiwidG9rZW4tdHlwZSI6ImFjY2Vzcy10b2tlbiIsImlzcyI6Ii9vYX
  V0aDIvdG9rZW4iLCJncm91cHMiOlsiY3JlYXRlIiwidXBkYXRlIiwiZGVsZXRlIl0sIm5iZi
  I6MTUzMzIyODUxOSwiZXhwIjoxNTMzMjI4ODE5LCJpYXQiOjE1MzMyMjg1MTksIm
  VtYWlsIjoiYWxleEBzdXBlcmJpei5jb20iLCJqdGkiOiI0ODAxODQ4MDFmNzgyOGNhIi
  widXNlcm5hbWUiOiJhbGV4In0.PZWPE-bXNzKbO6aoEqWxE....apj8sxtIBP1rgFIU8ZQ

In its JSON format, a JWT token looks like this for the header

{
  "kid": "my-rsa-key",
  "cty": "json",
  "typ": "JWT",
  "alg": "RS256"
}

And like this for the payload

{
  "sub": "alex",
  "token-type": "access-token",
  "iss": "/oauth2/token",
  "groups": [
  "create",
  "update",
  "delete"
  ],
  "nbf": 1533228519,
  "exp": 1533228819,
  "iat": 1533228519,
  "email": "[email protected]",
  "jti": "480184801f7828ca",
  "username": "alex"
}

Why do we need MicroProfile JWT?

A frustrating aspect of many standards is they often offer an excessive number of choices. This is definitely the case with JWT, which allows for several types of digital signatures and many possibilities for claims. While the possibilities are infinite, infinite options mean infinite opportunities for interoperability issues.

A critical goal of MicroProfile JWT is to take an opinionated perspective on just enough of these options such that basic interoperability across the enterprise can be achieved in a way that favors microservices specifically.

RSA-SHA256 is required

It is a critical goal of MicroProfile JWT that the JWT can be both verified and propagated by each microservice. MicroProfile JWT chooses RSA-based digital signatures for this purpose.

The RSA public key of the Authorization Server, which creates the JWTs, can be installed on all the microservices in advance of any actual HTTP requests. When calls come into a microservice, it will use this RSA public key to verify the JWT and determine if the caller’s identity is valid. If any HTTP calls are made by the microservice, it should pass the JWT in those calls, propagating the caller’s identity forward to other microservices.

Claims required

The JWT specification lists several “registered claims” to achieve specific goals. All of them, however, are optional. The question is: what good is a standard identity token if all the data in it is optional?

The MicroProfile JWT specification requires that specific claims be present so microservices can rely upon them for verification and role-based authorization. The most critical of these are:

  • exp: When should this JWT expire? This will be used by the MicroProfile JWT implementation to ensure old tokens are rejected.
  • sub: Who does this token represent? This will be the value returned from any getCallerPrinciple() calls made by the application.
  • groups: What can this person do? This will be the value used in any isCallerInRole() calls made by the application and any @RolesAllowed checks.

Without these claims, there would not be enough agreement in the architecture for stateless security to happen in a truly interoperable way.

Microprofile JWT also supports extensibility through the defined IANA JWT Assignments (pre-defined set of claims) or any custom claims.

Non-Goals of MicroProfile JWT

Understanding where specifications end is a critical part of seeing what they do. In the world of security, the MicroProfile JWT specification is strictly focused on a microservice’s ability to verify JWTs and does not define:

  • JWT Creation: Tokens will typically be created by a dedicated service in the enterprise such as an API Gateway like Tribestream or an identity provider like Okta.
  • RSA Public Key Distribution: Means to distribute the public key of the gateway or identity provider is out of scope for MicroProfile JWT. Like TLS/SSL certificates they do not change often and distributing them manually and installing them in docker images is a common practice.
  • Automatic JWT Propagation: Microservices using MicroProfile JWT have a guaranteed and standard way to obtain the JWT on any incoming calls. However, propagation must be done by the microservice itself in the application code by placing the JWT in the Authorization header of any outgoing HTTP calls.

Implementing stateless security with Microprofile JWT

MicroProfile JWT fulfills the requirements of stateless microservices architecture and solves the HTTP Session clustering issue by pushing the state on the caller side as opposed to maintaining the state on the server. On the server side, it becomes trivial to pass the security context (aka the JWT) as a header.

After a user or caller has logged into the gateway or identity provider and obtained a JWT, enforcing the JWT on future calls by the user involves 3 main steps:

  • Authenticate the caller: identify the caller by reading a standard claim (e.g. username) in the JWT and validate the signature of the JWT
  • Authorize the caller: by using the roles of the group listed in the claim, enforce access control in the application
  • Propagate caller context: pass the JWT token in subsequent calls so other microservices involved can also service the request

Figure 1, shows the big picture of how it looks like all together.

Inside the MicroProfile JWT API

The MicroProfile JWT implementation itself will handle the verification of the JWT found in the Authorization header of incoming HTTP calls. After verification, the application can use these existing Java EE APIs to reference data in the JWT.

  • javax.ws.rs.core.SecurityContext.getUserPrincipal()
  • javax.ws.rs.core.SecurityContext.isUserInRole(String)
  • javax.servlet.http.HttpServletRequest.getUserPrincipal()
  • javax.servlet.http.HttpServletRequest.isUserInRole(String)
  • javax.ejb.SessionContext.getCallerPrincipal()
  • javax.ejb.SessionContext.isCallerInRole(String)
  • javax.security.jacc.PolicyContext.getContext("javax.security.auth.Subject.container")
  • javax.security.enterprise.identitystore.IdentityStore.getCallerGroups(CredentialValidationResult)
  • @javax.annotation.security.RolesAllowed

All API references to principal are mapped to the sub claim of the caller’s JWT. All API references to roles or groups are mapped to the groups claim of the caller’s JWT.

In addition to providing this deep level of integration, MicroProfile JWT defines two APIs for developers to leverage JWT data in their application code.

  • org.eclipse.microprofile.jwt.JsonWebToken interface which provides means to “lookup” data from the JWT or obtain the JWT itself as an unparsed string for propagation.
  • @org.eclipse.microprofile.jwt.Claim annotation which provides CDI-based dependency injection of claims from the JWT

JsonWebToken interface

CDI beans in the application that are @RequestScoped may use dependency injection to obtain the JWT of the currently executing caller. The implementation of JsonWebToken will be supplied by the MicroProfile JWT provider and will represent an already verified and decoded JWT.

import javax.ws.rs.Path;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/pizza")
@RequestScoped
public class PizzaEndpoint {

   @Inject
   private JsonWebToken callerPrincipal;

As of MicroProfile JWT 1.1, this interface is defined as follows:

package org.eclipse.microprofile.jwt;

public interface JsonWebToken extends Principal {

    /**
     * Returns the unique name of this principal. This either comes from the upn
     * claim, or if that is missing, the preferred_username claim. Note that for
     * guaranteed interoperability a upn claim should be used.
     *
     * @return the unique name of this principal.
     */
    @Override
    String getName();

    /**
     * Get the raw bearer token string originally passed in the authentication
     * header
     * @return raw bear token string
     */
    default String getRawToken() {
        return getClaim(Claims.raw_token.name());
    }

    /**
     * The iss(Issuer) claim identifies the principal that issued the JWT
     * @return the iss claim.
     */
    default String getIssuer() {
        return getClaim(Claims.iss.name());
    }

    /**
     * The aud(Audience) claim identifies the recipients that the JWT is
     * intended for.
     * @return the aud claim or null if the claim is not present
     */
    default Set<String> getAudience() {
        return getClaim(Claims.aud.name());
    }

    /**
     * The sub(Subject) claim identifies the principal that is the subject of
     * the JWT. This is the token issuing
     * IDP subject, not the
     *
     * @return the sub claim.
     */
    default String getSubject() {
        return getClaim(Claims.sub.name());
    }

    /**
     * The jti(JWT ID) claim provides a unique identifier for the JWT.
     The identifier value MUST be assigned in a manner that ensures that
     there is a negligible probability that the same value will be
     accidentally assigned to a different data object; if the application
     uses multiple issuers, collisions MUST be prevented among values
     produced by different issuers as well.  The "jti" claim can be used
     to prevent the JWT from being replayed.
     * @return the jti claim.
     */
    default String getTokenID() {
        return getClaim(Claims.jti.name());
    }

    /**
     * The exp (Expiration time) claim identifies the expiration time on or
     * after which the JWT MUST NOT be accepted
     * for processing in seconds since 1970-01-01T00:00:00Z UTC
     * @return the exp claim.
     */
    default long getExpirationTime() {
        return getClaim(Claims.exp.name());
    }

    /**
     * The iat(Issued at time) claim identifies the time at which the JWT was
     * issued in seconds since 1970-01-01T00:00:00Z UTC
     * @return the iat claim
     */
    default long getIssuedAtTime() {
        return getClaim(Claims.iat.name());
    }

    /**
     * The groups claim provides the group names the JWT principal has been
     * granted.
     *
     * This is a MicroProfile specific claim.
     * @return a possibly empty set of group names.
     */
    default Set<String> getGroups() {
        return getClaim(Claims.groups.name());
    }

    /**
     * Access the names of all claims are associated with this token.
     * @return non-standard claim names in the token
     */
    Set<String> getClaimNames();

    /**
     * Verify is a given claim exists
     * @param claimName - the name of the claim
     * @return true if the JsonWebToken contains the claim, false otherwise
     */
    default boolean containsClaim(String claimName) {
        return claim(claimName).isPresent();
    }

    /**
     * Access the value of the indicated claim.
     * @param claimName - the name of the claim
     * @return the value of the indicated claim if it exists, null otherwise.
     */
    <T> T getClaim(String claimName);

    /**
     * A utility method to access a claim value in an {@linkplain Optional}
     * wrapper
     * @param claimName - the name of the claim
     * @param <T> - the type of the claim value to return
     * @return an Optional wrapper of the claim value
     */
    default <T> Optional<T> claim(String claimName) {
        return Optional.ofNullable(getClaim(claimName));
    }
}

@Claim

Alternatively, CDI beans in the application that are @RequestScoped may use dependency injection to obtain individual claims from the caller’s JWT using the @Claim qualifier.

import javax.ws.rs.Path;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;

@Path("/pizza")
@RequestScoped
public class PizzaEndpoint {

    @Inject
    @Claim("raw_token")
    private String rawToken;

    @Inject
    @Claim("groups")
    private Set<String> groups;

    @Inject
    @Claim("exp")
    private long expiration;

    @Inject
    @Claim("sub")
    private String subject;

MicroProfile JWT implementations are required to support the following data types:

  • boolean
  • byte
  • char
  • short
  • int
  • long
  • float
  • double
  • java.lang.String
  • java.lang.Boolean
  • java.lang.Byte
  • java.lang.Character
  • java.lang.Short
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Float
  • java.lang.Double
  • javax.json.JsonValue.TRUE/FALSE
  • javax.json.JsonString
  • javax.json.JsonNumber
  • javax.json.JsonArray
  • javax.json.JsonObject
  • java.util.Optional wrapper of the above types

ClaimValue

In order to support CDI beans that are not @RequestScoped, the MicroProfile JWT API introduces an org.eclipse.microprofile.jwt.ClaimValue interface that may be used with the @Claim qualifier.

import javax.ws.rs.Path;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.ClaimValue;

@Path("/sandwiches")
@ApplicationScoped
public class sandwichesEndpoint {

    @Inject
    @Claim("raw_token")
    private ClaimValue<String> rawToken;

    @Inject
    @Claim("exp")
    private ClaimValue<Long> expiration;

    @Inject
    @Claim("sub")
    private ClaimValue<String> subject;

The intent of the ClaimValue interface is to provide a proxy to the claim. During a request the @ApplicationScoped bean may obtain the value of a claim by calling getValue() on the ClaimValue proxy. When this happens the MicroProfile JWT provider will find the caller’s JWT in the current request, get the corresponding claim and return it.

Propagating the JWT on outgoing calls

As mentioned, it is the application’s responsibility to propagate the caller’s JWT on any outgoing calls. This will vary depending on the HTTP Client library. To help everyone get started, we will provide some boilerplate code you can copy/paste using Apache Commons HTTPClient 4.5.

In efforts to show the most realistic boilerplate code, we’re going to assume the application has a CDI @Produces method that sets up the CloseableHttpClient with proper connection pooling so we are not creating new connections on every request.

import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.eclipse.microprofile.jwt.Claim;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.*;
import javax.ws.rs.*;
import java.io.IOException;

@Path("/pizza")
@RequestScoped
public class PizzaEndpoint {

    @Inject
    @Claim("raw_token")
    private String rawToken;

    @Inject
    @Claim("address") // Custom claim
    private JsonObject address;

    @Inject
    private CloseableHttpClient httpClient;

    @POST
    @Path("theworks")
    public void orderPizzaWithEverything() throws IOException {

        final HttpPost scheduleDelivery = new HttpPost("http://example.com/api/delivery/schedule");

        // Propagate the caller's JWT
        scheduleDelivery.setHeader("Authorization", rawToken);

        final JsonObject order = Json.createObjectBuilder()
                .add("pizza", "the works")
                .add("quantity", 1)
                .add("address", address)
                .build();

        scheduleDelivery.setEntity(new StringEntity(order.toString()));

        try (CloseableHttpResponse response = httpClient.execute(scheduleDelivery)) {
            // check the response
        }
    }
}

As an added bonus, we show the use of a custom JWT claim called address, which we’ll assume holds a JSON representation of the caller’s address. Without this custom claim, we would be using a database to get the caller’s address. How custom claims can be added to a JWT is specific to the gateway or identity provider, but it is a widely supported feature. The next article in the series will show how to do it with TomEE and the Tribestream API Gateway.

When used properly, custom claims have the potential to reduce state in applications beyond security. You may customize the JWT with user information such as email, language, and subscription details, so you can then use the trustable information in the JWT during processing. You can, for instance, decide to route gold-level subscriptions to a dedicated set of servers.

For all intents and purposes, a fair way to describe a JWT is as “a signed HTTP cookie.” When you think of it from this perspective, the possibilities are endless.

The Pros and Cons to using JWT

There are some downsides to using JWT to store session context including:

  • Base64 encoding isn’t encryption: Because base64 is not encryption the caller and everyone in the middle can, at any point, read the content of the JWT. If you
    want to protect your data, you also need to encrypt part or the whole JWT using JSON Web Encryption another standard like JSON Web Signature but for encryption rather than
    signing.
  • Larger Payload: even in its most minimal form, a serialized JWT is way bigger than a regular cookie or SessionID. Although bandwidth is less of an issue today,
    it’s something to keep in mind. The more you put in the JWT, the more you have to send with each and every communication on the wire.
  • Token Expiration: keep in mind that the token can be valid but may contain outdated information. Make sure to properly configure the expiration policy of your token so
    that a stale token can be detected and refreshed.

On the other hand, there are a lot of benefits to using JWT

  • JSON: because it’s JSON based, it’s simple and lightweight with plenty of libraries that support it.
  • Speed and Reliability: it does not require a third party call which, in the context of a distributed system (microservices for instance), can be slow and create a single
    point of failure (either LDAP, Database, or your identity provider).
  • Secure: it is secure if you are, at the very least, using JSON Web Signature and possibly JSON Web Encryption.
  • Claims: it easy for parties to agree on a common set of claims for performing authentication, authorization and more.

Next Time

Microprofile JWT defines a common integration with Java EE and Jakarta EE allowing efficient use of JWT in microservice development. It defines a standard means for JWTs to be verified transparently by the MicroProfile JWT provider and for that data to be exposed to applications via Context and Dependency Injection (CDI).

In the next article on MicroProfile JWT, we will provide a tutorial so that you can deploy and play with this feature using TomEE!

Jean-Louis Monteiro

Jean-Louis Monteiro

Jean-Louis is the Director of Engineering at Tomitribe. He is passionate about Open Source, an active contributor to Apache projects, a Java Community Process (JCP) member, and both the EJB 3.2 and the Java EE Security API expert groups. Jean-Louis loves speaking at Java User Groups, ApacheCon, JavaOne and Devoxx.
jlouismonteiro

Leave a Reply