Eclipse MicroProfile - JWT RBAC Security (MP-JWT)

Overview

The first part of this article describes the background and motivation for the MicroProfile JWT RBAC security specification (MP-JWT). The second part of the article will get into the specifics of the specification in terms of the JSON web token requirements, APIs. The third and final part will demonstrate example usage.

The security requirements that involve microservice architectures are strongly related with RESTful security. In a RESTful architecture style, services are usually stateless and any security state associated with a client is sent to the target service on every request in order to allow services to re-create a security context for the caller and perform both authentication and authorization checks.

For RESTful based microservices, security tokens offer a very lightweight and interoperable way to propagate identities across different services, where:

  • Services don’t need to store any state about clients or users
  • Services can verify the token validity if token follows a well known format. Otherwise, services may invoke a separated service.
  • Services can identify the caller by introspecting the token. If the token follows a well known format, services are capable to introspect the token by themselves, locally. Otherwise, services may invoke a separated service.
  • Services can enforce authorization policies based on any information within a security token
  • Support for both delegation and impersonation of identities

Today, the most common solutions involving RESTful and microservices security are based on OAuth2, OpenID Connect(OIDC) and JSON Web Tokens(JWT) standards.

Token Based Authentication

Token Based Authentication mechanisms allow systems to authenticate, authorize and verify identities based on a security token. Usually, the following entities are involved:

  • Issuer - Responsible for issuing security tokens as a result of successfully asserting an identity (authentication). Issuers are usually related with Identity Providers.
  • Client - Represented by an application to which the token was issued for. Clients are usually related with Service Providers. A client may also act as an intermediary between a subject and a target service (delegation).
  • Subject - The entity to which the information in a token refers to.
  • Resource Server - Represented by an application that is going to actually consume the token in order to check if a token gives access or not to a protected resource.

Independent of the token format or protocol in use, from a service perspective, token based authentication is based on the following steps:

1. Extract security token from the request

  • For RESTful services, this is usually achieved by obtaining the token from the Authorization header.

2. Perform validation checks against the token

  • This step usually depends on the token format and security protocol in use. The objective is make sure the token is valid and can be consumed by the application. It may involve signature, encryption and expiration checks.

3.Introspect the token and extract information about the subject

  • This step usually depends on the token format and security protocol in use. The objective is to obtain all the necessary information about the subject from the token.

4.Create a security context for the subject

  • Based on the information extracted from the token, the application creates a security context for the subject in order to use the information wherever necessary when serving protected resources.

Using JWT Bearer Tokens to Protect Services

For the current MP-JWT specification, use cases are based on a scenario where services belong to the same security domain. This is avoids dealing with the complexities associated with federation of security domains. With that in mind, we assume that any information carried along with a token could be understood and processed (without any security breaches) by the different services involved.

The use case can be described as follows:

A client sends a HTTP request to Service A including the JWT as a bearer token:

GET /resource/1 HTTP/1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM

On the server, a token-based authentication mechanism in front of Service A perform all steps described in the Token Based Authentication section. As part of the security context creation, the server establishes role and group mappings for the subject based on the JWT claims. The role to group mapping is fully configurable by the server along the lines of the Java EE RBAC security model.

JWT tokens follow a well defined and known standard that is becoming the most common token format to protect services. In addition to providing a token format, it defines additional security aspects like signature and encryption based on another set of standards including JSON Web Signature (JWS) and JSON Web Encryption (JWE).

There are several reasons why JWT is becoming so widely adopted:

  • Token validation doesn’t require an additional trip and can be validated locally by each service
  • Given its JSON nature, it is solely based on claims or attributes to carry authentication and authorization information about a subject.
  • Makes easier to support different types of access control mechanisms such as ABAC, RBAC, Context-Based Access Control, etc.
  • Message-level security using signature and encryption as defined by both JWS and JWE standards.
  • Given its JSON nature, processing JWT tokens becomes trivial and lightweight. Especially if considering Java EE standards such as JSON-P or the different third-party libraries out there such as Nimbus, Jackson, etc.
  • Parties can easily agree on a specific set of claims in order to exchange both authentication and authorization information. Defining this along with the Java API and mapping to JAX-RS APIs are the primary tasks of the MP-JWT specification.
  • Widely adopted by different Single Sign-On solutions and well known standards such as OpenID Connect given its small overhead and ability to be used across different different security domains (federation).

The MP-JWT Specification

The focus of the MP-JWT specification is the definition of the required format of the JWT used as the basis for interoperable authentication and authorization. The source for the specification, the API and the TCK are available from the Eclipse microprofile-jwt-auth repository.

The maximum utility of MP-JWT as a token format depends on the agreement between both identity providers and service providers. This means identity providers - responsible for issuing tokens - should be able to issue tokens using the MP-JWT format in a way that service providers can understand in order to introspect the token and gather information about a subject. To that end, the requirements for the MicroProfile JWT are:

  1. Be usable as an authentication token.
  2. Be usable as an authorization token that contains Java EE application level roles indirectly granted via a groups claim.
  3. Can support additional standard claims described in IANA JWT Assignments as well as non-standard claims.

To meet those requirements, we introduce 2 new claims to the MP-JWT:

  • "upn": A human readable claim that uniquely identifies the subject or user principal of the token, across the MicroProfile services the token will be accessed with.
  • "groups": The token subject's group memberships that will be mapped to Java EE style application level roles in the MicroProfile service container.
This specification has the following Java EE API dependencies:
  • JAX-RS 2.0.1
  • JSON-P 1.0
  • CDI 1.2
  • Common Annotations for the Java Platform 1.2

Minimum MP-JWT Required Claims

The required minimum set of MP-JWT claims is based on claims from RFC7519 along with the two new MP-JWT specific claims. The required set of claims is:

Claim Name

Description

Reference

typ

This JOSE header parameter identifies the token format and must be "JWT"

RFC7519, Section 5.1

alg

This JOSE header parameter identifies the cryptographic algorithm used to secure the JWT. MP-JWT requires the use of the RSASSA-PKCS1-v1_5 SHA-256 algorithm and must be specified as "RS256"

RFC7515, Section 4.1.1

kid

This JOSE header parameter is a hint indicating which key was used to secure the JWT.

RFC7515, Section-4.1.4

iss

The token issuer

RFC7519, Section 4.1.1

sub

Identifies the principal that is the subject of the JWT. See the "upn" claim for how this relates to the runtime java.security.Principal

RFC7519, Section 4.1.2

aud

Identifies the recipients that the JWT is intended for

RFC7519, Section 4.1.3

exp

Identifies the expiration time on or after which the JWT MUST NOT be accepted for processing*

RFC7519, Section 4.1.4

iat

Identifies the time at which the issuer generated the JWT*

RFC7519, Section 4.1.6

jti

Provides a unique identifier for the JWT

RFC7519, Section 4.1.7

upn

Provides the user principal name in the java.security.Principal interface**

MP-JWT 1.0 specification

groups

Provides the list of group names that have been assigned to the principal of the MP-JWT. This typically will require a mapping at the application container level to application deployment roles, but a one-to-one between group names and application role names is required to be performed in addition to any other mapping.

MP-JWT 1.0 specification

* The NumericDate used by `exp`, `iat`, and other date related claims is a JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds.

**There is fallback logic to leverage existing standard claims if the upn claim is missing. An implementation should first look to the OIDC preferred_username, and if that is missing, the "sub" claim should be used.

An example minimal MP-JWT in JSON would be:

{
    "typ": "JWT",
   "alg": "RS256",
   "kid": "abc-1234567890"
}
{
      "iss": "https://server.example.com",
      "aud": "s6BhdRkqt3",
      "jti": "a-123",
      "exp": 1311281970,
      "iat": 1311280970,
      "sub": "24400320",
      "upn": "jdoe@server.example.com",
      "groups": ["red-group", "green-group", "admin-group", "admin"],
}

Additional Claims

The MP-JWT can contain any number of other custom and standard claims. An example MP-JWT that contains additional "auth_time", "preferred_username", "acr", "nbf" and a custom "roles" claims is:

{
   "typ": "JWT",
   "alg": "RS256",
   "kid": "abc-1234567890"
}
{
  "iss": "https://server.example.com",
  "aud": "s6BhdRkqt3",
  "exp": 1311281970,
  "iat": 1311280970,
  "sub": "24400320",
  "upn": "jdoe@server.example.com",
  "groups: ["red-group", "green-group", "admin-group"],
  "roles": ["auditor", "administrator"],
  "jti": "a-123",
  "auth_time": 1311280969,
  "preferred_username": "jdoe",
  "acr": "phr",
  "nbf":  1311288970
}

The JsonWebToken Interface

This specification defines a JsonWebToken java.security.Principal interface extension that makes this set of required claims available via get style accessors. The MP-JWT 1.0 JsonWebToken interface definition is:

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

The Claims Enumeration Utility Class, and the Set of Claim Value Types

The Claims utility class encapsulates an enumeration of all the standard JWT related claims along with a description and the required Java type for the claim as returned from the JsonWebToken#getClaim(String) method.

public enum Claims {
   // The base set of required claims that MUST have non-null values in the JsonWebToken
   iss("Issuer", String.class),
   sub("Subject", String.class),
   aud("Audience", Set.class),
   exp("Expiration Time", Long.class),
   iat("Issued At Time", Long.class),
   jti("JWT ID", String.class),
   upn("MP-JWT specific unique principal name", String.class),
   groups("MP-JWT specific groups permission grant", Set.class),
   raw_token("MP-JWT specific original bearer token", String.class),

   // The IANA registered, but MP-JWT optional claims
   nbf("Not Before", Long.class),
   auth_time("Time when the authentication occurred", Long.class),

...
   public String getDescription() {
       return description;
   }

   public Class<?> getType() {
       return type;
   }
}

Custom claims not defined by the Claims enum are required to be valid JSON-P javax.json.JsonValue subtypes. The current complete set of valid claim types is therefore, (excluding the invalid Claims.UNKNOWN Void type):

  • java.lang.String
  • java.lang.Long
  • java.lang.Boolean
  • java.util.Set<java.lang.String>
  • javax.json.JsonValue.TRUE/FALSE
  • javax.json.JsonString
  • javax.json.JsonNumber
  • javax.json.JsonArray
  • javax.json.JsonObject

Mapping MP-JWT Tokens to Java EE Container APIs

The requirements of how a JWT should be exposed via the various Java EE container APIs is discussed in this section. For the 1.0 release, the only mandatory container integration is with the JAX-RS container, and CDI injection of the MP-JWT types.

CDI Injection Requirements

This section describes the requirements for MP-JWT implementations with regard to the injection of MP-JWT tokens and their associated claim values.

Injection of `JsonWebToken`

An MP-JWT implementation must support the injection of the currently authenticated caller as a JsonWebToken as shown in this code fragment:

@Path("/endp")
@DenyAll
@RequestScoped
public class RolesEndpoint {

   @Inject
   private JsonWebToken callerPrincipal;

Injection of JsonWebToken Claims via Raw, ClaimValue and JSON-P Types

This specification requires support for injection of claims from the current JsonWebToken using the org.eclipse.microprofile.jwt.Claim qualifier:

/**
* Annotation used to signify an injection point for a {@link ClaimValue} from
* a {@link JsonWebToken}
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Claim {
   /**
    * The value specifies the id name the claim to inject
    * @return the claim name
    * @see JsonWebToken#getClaim(String)
    */
   @Nonbinding
   String value() default "";

   /**
    * An alternate way of specifying a claim name using the {@linkplain Claims}
    * enum
    * @return the claim enum
    */
   @Nonbinding
   Claims standard() default Claims.UNKNOWN;
}

with `@Dependent` scoping. MP-JWT implementations are required to support injection of the claim values using any of:

  • The raw type associated with the JsonWebToken claim value as defined in the Claims enum.
  • org.eclipse.microprofile.jwt.ClaimValue wrapper.
  • javax.json.JsonValue JSON-P subtypes.

The org.eclipse.microprofile.jwt.ClaimValue interface is:

/**
* A representation of a claim in a {@link JsonWebToken}
* @param <T> the expected type of the claim
*/
public interface ClaimValue<T> extends Principal {

   /**
    * Access the name of the claim.
    * @return The name of the claim as seen in the JsonWebToken content
    */
   @Override
   public String getName();

   /**
    * Access the value of the claim.
    * @return the value of the claim.
    */
   public T getValue();
}

The following example code fragment illustrates various examples of injecting different types of claims using a range of generic forms of the ClaimValue, JsonValue subtypes, as well as the raw claim types

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.ClaimValue;
import org.eclipse.microprofile.jwt.Claims;

@Path("/endp")
@DenyAll
@RequestScoped
public class RolesEndpoint {
...

   // Raw types
   @Inject
   @Claim(standard = Claims.raw_token)
   private String rawToken;
   @Inject // <1>
   @Claim(standard=Claims.iat)
   private Long issuedAt;

   // ClaimValue wrappers
   @Inject // <2>
   @Claim(standard = Claims.raw_token)
   private ClaimValue<String> rawTokenCV;
   @Inject
   @Claim(standard = Claims.iss)
   private ClaimValue<String> issuer;
   @Inject
   @Claim(standard = Claims.jti)
   private ClaimValue<String> jti;
   @Inject // <3>
   @Claim("jti")
   private ClaimValue<Optional<String>> optJTI;
   @Inject
   @Claim("jti")
   private ClaimValue objJTI;
   @Inject // <4>
   @Claim("aud")
   private ClaimValue<Set<String>> aud;
   @Inject
   @Claim("groups")
   private ClaimValue<Set<String>> groups;
   @Inject // <5>
   @Claim(standard=Claims.iat)
   private ClaimValue<Long> issuedAtCV;
   @Inject
   @Claim("iat")
   private ClaimValue<Long> dupIssuedAt;
   @Inject
   @Claim("sub")
   private ClaimValue<Optional<String>> optSubject;
   @Inject
   @Claim("auth_time")
   private ClaimValue<Optional<Long>> authTime;
   @Inject // <6>
   @Claim("custom-missing")
   private ClaimValue<Optional<Long>> custom;
   //
   @Inject
   @Claim(standard = Claims.jti)
   private Instance<String> providerJTI;
   @Inject // <7>
   @Claim(standard = Claims.iat)
   private Instance<Long> providerIAT;
   @Inject
   @Claim("groups")
   private Instance<Set<String>> providerGroups;
   //
   @Inject
   @Claim(standard = Claims.jti)
   private JsonString jsonJTI;
   @Inject
   @Claim(standard = Claims.iat)
   private JsonNumber jsonIAT;
   @Inject // <8>
   @Claim("roles")
   private JsonArray jsonRoles;
   @Inject
   @Claim("customObject")
   private JsonObject jsonCustomObject;

<1> Injection of a non-proxyable raw type like java.lang.Long must happen in a RequestScoped bean as the producer will have dependendent scope.

<2> Injection of the raw MP-JWT token string.

<3> Injection of the jti token id as an `Optional<String>` wapper.

<4> Injection of the aud audience claim as a Set<String>. This is the required type as seen by looking at the Claims.aud enum value's Java type member.

<5> Injection of the issued at time claim using an @Claim that references the claim name using the Claims.iat enum value.

<6> Injection of a custom claim that does exist will result in an Optional<Long>

value for which isPresent() will return false.

<7> Another injection of a non-proxyable raw type like java.lang.Long, but the use of the javax.enterprise.inject.Instance interface allows for injection to occur in non-RequestScoped contexts.

<8> Injection of a JsonArray of role names via a custom "roles" claim.

The example shows that one may specify the name of the claim using a string or a Claims enum value. The string form would allow for specifying non-standard claims while the Claims enum approach guards against typos and misspellings.

JAX-RS Container API Integration

The behavior of the following JAX-RS security related methods is required for MP-JWT implementations.

javax.ws.rs.core.SecurityContext.getUserPrincipal()

The java.security.Principal returned from these methods MUST be an instance of org.eclipse.microprofile.jwt.JsonWebToken.

javax.ws.rs.core.SecurityContext#isUserInRole(String)

This method MUST return true for any name that is included in the MP-JWT "groups" claim, as well as for any role name that has been mapped to a group name in the MP-JWT "groups" claim.

Mapping from @RolesAllowed

Any role names used in @RolesAllowed or equivalent security constraint metadata that match names in the role names that have been mapped to group names in the MP-JWT "groups" claim, MUST result in an allowing authorization decision wherever the security constraint has been applied.

Example Usage

Let’s look at a concrete example. The source code repository is available at https://github.com/MicroProfileJWT/eclipse-newsletter-sep-2017. To get things setup, follow these steps:

  1. Download the wildfly-swarm MP_12-RC3 release from: https://github.com/MicroProfileJWT/wildfly-swarm-mp1.2/releases/tag/MP_12-RC3
  2. Follow the instructions on the release page to either download the source and build it, or to download the maven artifacts and unpack into your local maven repository.
  3. git clone https://github.com/MicroProfileJWT/eclipse-newsletter-sep-2017
  4. cd eclipse-newsletter-sep-2017
  5. Run mvn test from the directory to build and run the test example

The example project is a maven based TestNG/Arquillian unit test that that deploys a JAX-RS application using Wildfly-Swarm. The structure of the application is shown in the following figure:

Figure 1, View of the example structure.

The files are

  • MyJaxrsApp.java - The JAX-RS Application class
  • MySecureWallet.java - A JAX-RS resource endpoint
  • MySecureWalletTest.java - An Arquillian/TestNg unit test to deploy and exercise the MySecureWallet endpoint
  • resources - test resources directory
  • web.xml - servlet metadata descriptor to define security constraints
  • jwt-roles.properties - a properties file used to define token group to application role mappings
  • privateKey.pem - the test private key used to sign the token
  • Project-defaults.yml - a Wildfly-Swarm configuration file that sets up the security domain
  • publicKey.pem - the test public key used to verify the token signature
  • Token1.json - the JSON content for test token 1
  • Token1-50000-limit.json the json content for test token 1 with an alternate warningLimit claim
  • Token2.json - the JSON content for test token 2
  • Token3.json - the JSON content for test token 3
  • Token-noaccess.json - the JSON content for a test token that should not map to any valid access roles.

Listing 1, MyJaxrsApp.java provides the JAX-RS Application class. In addition to the standard JAX-RS/CDI annotations that define the application root path and scope, there is the MP-JWT @LoginConfig which declares the “MP-JWT” authentication method, and a “jwt-jaspi” realmName. The MP-JWT runtime will use this information to setup the authentication mechanism to be based on MicroProfile JWT bearer tokens.

Listing 1, MyJaxrsApp.java

package org.eclipse.microprofile.test.jwt;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.Path;
import javax.ws.rs.core.Application;

import org.eclipse.microprofile.auth.LoginConfig;

// We set the authentication method to the MP-JWT for the MicroProfile JWT method
// the realmName maps the security-domains setting in the project-defaults.yml
@LoginConfig(authMethod = "MP-JWT", realmName = "jwt-jaspi")
@ApplicationScoped
@ApplicationPath("/wallet")
public class MyJaxrsApp extends Application {
}

The JAX-RS endpoint resource is shown in Listing 2, MySecureWallet.java. This is a hypothetical online wallet that provides operations for viewing the wallet balance, debiting money from the wallet, and crediting money to the wallet.

Listing 2, MySecureWallet.java

package org.eclipse.microprofile.test.jwt;

import java.math.BigDecimal;
import java.util.Optional;

import javax.annotation.security.DeclareRoles;
import javax.annotation.security.DenyAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.JsonWebToken;

@ApplicationScoped
@DeclareRoles({"ViewBalance", "Debtor", "Creditor", "Debtor2", "BigSpender"})
@Path("/")
@DenyAll
public class MySecureWallet {
   private double bigSpenderLimit = 1000;
   private BigDecimal usdBalance = new BigDecimal("100000.0000");
   private BigDecimal bitcoinXrate = new BigDecimal("4538.0000");
   private BigDecimal ethereumXrate = new BigDecimal("328.0000");
   @Inject
   private JsonWebToken jwt;
   @Inject
   @Claim("warningLimit")
   private Instance<Optional<JsonNumber>> warningLimitInstance;

   @GET
   @Path("/balance")
   @Produces(MediaType.APPLICATION_JSON)
   @RolesAllowed({"ViewBalance", "Debtor", "Creditor"})
   public JsonObject getBalance() {
       return generateBalanceInfo();
   }

   @GET
   @Path("/debit")
   @Produces(MediaType.APPLICATION_JSON)
   @RolesAllowed({"Debtor", "Debtor2"})
   public Response debit(@QueryParam("amount") String amount,
                         @Context SecurityContext securityContext) {
       Double damount = Double.valueOf(amount);
       if(damount > bigSpenderLimit) {
           if(securityContext.isUserInRole("BigSpender")) {
               // Validate the spending limit from the token claim
               JsonNumber spendingLimit = jwt.getClaim("spendingLimit");
               if(spendingLimit == null || spendingLimit.doubleValue() < damount) {
                   return Response.status(Response.Status.BAD_REQUEST).build();
               }
           } else {
               return Response.status(Response.Status.FORBIDDEN).build();
           }
       }
       usdBalance = usdBalance.subtract(new BigDecimal(amount));
       return Response.ok(generateBalanceInfo()).build();
   }

   @GET
   @Path("/credit")
   @Produces(MediaType.APPLICATION_JSON)
   @RolesAllowed("Creditor")
   public JsonObject credit(@QueryParam("amount") String amount) {
       usdBalance = usdBalance.add(new BigDecimal(amount));
       return generateBalanceInfo();
   }

   private JsonObject generateBalanceInfo() {
       BigDecimal balanceInBitcoins = usdBalance.divide(bitcoinXrate, BigDecimal.ROUND_HALF_EVEN);
       BigDecimal balanceInEthereum = usdBalance.divide(ethereumXrate, BigDecimal.ROUND_HALF_EVEN);
       JsonObjectBuilder result = Json.createObjectBuilder()
               .add("usd", usdBalance)
               .add("bitcoin", balanceInBitcoins)
               .add("ethereum", balanceInEthereum)
               ;

       Optional<JsonNumber> warningLimit = warningLimitInstance.get();
       if(warningLimit.isPresent() && warningLimit.get().doubleValue() > usdBalance.doubleValue()) {
           String warningMsg = String.format("balance is below warning limit: %s", warningLimit.get());
           result.add("warning", warningMsg);
       }

       return result.build();
   }
}

The wallet endpoint is using JAX-RS, CDI and Java EE security annotations to define behaviors. The first MP-JWT feature to notice is the injection of the JsonWebToken and a warningLimit claim value:

@Inject
private JsonWebToken jwt;
@Inject
@Claim("warningLimit")
private Instance<Optional<JsonNumber>> warningLimitInstance;

The JsonWebToken is an interface into the MP-JWT bearer token of the currently authenticated caller. The warningLimitInstance is a view into a custom token claim named “warningLimit”. Here we are using the CDI Instance interface to provide access to the claim because MySecureWallet is @ApplicationScoped while claim values are produced with @Dependent scope while being associated with the @RequestScoped token. Without the Instance interface, the CDI container would bind the warningLimit claim value to the first value seen when the MySecureWallet resource was created. The type of the warningLimit claim value is Optional</JsonNumber>. We are using Optional because not every caller’s token may include this custom claim. The underlying type is a JsonNumber because custom claim types are represented as the JSON-P type associated with the token’s JSON payload.</JsonNumber>

The injected JsonWebToken is used in the debit endpoint:

if(damount > bigSpenderLimit) {
   if(securityContext.isUserInRole("BigSpender")) {
       // Validate the spending limit from the token claim
       JsonNumber spendingLimit = jwt.getClaim("spendingLimit");
       if(spendingLimit == null || spendingLimit.doubleValue() < damount) {
           return Response.status(Response.Status.BAD_REQUEST).build();
       }
   } else {
       return Response.status(Response.Status.FORBIDDEN).build();
   }
}

Here a check is made to see if the debit amount is above a bigSpenderLimit value, and if true, and the caller has the “BigSpender” role, is the debit amount under the “spendingLimit” claim value from the JsonWebToken.

The warningLimitInstance is used in the generateBalanceInfo method:
Optional<JsonNumber> warningLimit = warningLimitInstance.get();
if (warningLimit.isPresent()) {
   if (warningLimit.get().doubleValue() > usdBalance.doubleValue()) {
       String warningMsg = String.format("balance is below warning limit: %s", 
warningLimit.get());
       result.add("warning", warningMsg);
   }
}

If the caller’s JsonWebToken has a “warningLimit” claim value, that value is compared to the current balance and if the balance is below the warningLimit, a warning message is added to the balance info JsonObject.

Role Handling

To understand how the MP-JWT token is used for authorization in the context of the Java EE security annotations used on the MySecureWallet endpoint, let’s first look at the Token1.json payload:

Listing 3, Token1.json

{
 "iss": "https://server.example.com",
 "jti": "a-123",
 "sub": "24400320",
 "upn": "jdoe@example.com",
 "preferred_username": "jdoe",
 "aud": "wallet",
 "exp": 1311281970,
 "iat": 1311280970,
 "auth_time": 1311280969,
 "groups": [
   "ViewBalance", "Debtor", "Creditor"
 ],
 "spendingLimit": 2500,
 "warningLimit": 90000
}

Here the “groups” claim is what conveys the base RBAC information. This provides the names of the groups(collections of role names) that the caller bearing the token is granted. The various @RolesAllowed uses define the role names that are allowed to access a protected endpoint. It was mentioned earlier that an MP-JWT implementation is required to provide a one-to-one mapping between the names in the “groups” claim to role names. Therefore, this token bearer will have at least the "ViewBalance", "Debtor", "Creditor" role names. MP-JWT containers are free to provide additional group to role mapping configuration. How this is done is specific to the container implementation, as this is a feature of Java EE security that was defined to be an implementation detail. In Wildfly-Swarm, this can be done by configuring a JAAS stack. The resources/project-defaults.yml file contains the following security.security-domains.jwt-jaspi.jaspi-authentication subtree:

login-module-stacks:
 roles-lm-stack:
   login-modules:
     # This stack performs the token verification and group to role mapping
     - login-module: rm
       code: org.wildfly.swarm.mpjwtauth.deployment.auth.jaas.JWTLoginModule
       flag: required
       module-options:
         rolesProperties: jwt-roles.properties

This defines a JWTLoginModule which is a JAAS login module that does authentication of the MP-JWT token by verifying the issuer, signer and expiration. It also performs the one-to-one group name to role name mapping of the token “groups” claim, and will augment the roles with any additional group name to role name mapping that is specified in the jwt-roles.properties file found in the deployment classpath. If you look at the resources/jwt-roles.properties file, it contains this entry:

Debtor=BigSpender

This says that the “Debtor” group will be assigned the role name “BigSpender”. This is in addition to the role name “Debtor”. If you look through all of the various *.json files for the test token contents, there is no “groups” claim that has the “BigSpender” name. That role name, which we saw being used in the debit endpoint logic, is assigned to tokens with the “Debitor” group through this secondary mapping.

The MySecureWalletTest Code

We will look at the deployment creation and the bigSpenderDebitBalanceFail test to get a feel for how the test code works. An Arquillian test of a container has a deployment archive that contains the application code and resources being tested. This is the deployment creation method for MySecureWalletTest:
/**
* Create a CDI aware JAX-RS application archive with our endpoints and
* @return the JAX-RS application archive
* @throws IOException - on resource failure
*/
@Deployment(testable=true)
public static WebArchive createDeployment() throws IOException {
   // Various system properties you can set to enable debug logging, debugging
   //System.setProperty("swarm.resolver.offline", "true");
   //System.setProperty("swarm.logging", "DEBUG");
   //System.setProperty("swarm.debug.port", "8888");

   // Get the public key of the token signer
   URL publicKey = MySecureWalletTest.class.getResource("/publicKey.pem");
   WebArchive webArchive = ShrinkWrap
           .create(WebArchive.class, "MySecureEndpoint.war")
           // Place the public key in the war as /MP-JWT-SIGNER - Wildfly-Swarm specific
           .addAsManifestResource(publicKey, "/MP-JWT-SIGNER")
           .addClass(MySecureWallet.class)
           .addClass(MyJaxrsApp.class)
           .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
           // Add Wildfly-Swarm specific configuration of the security domain
           .addAsResource("project-defaults.yml", "/project-defaults.yml")
           .addAsWebInfResource("jwt-roles.properties", "classes/jwt-roles.properties")
           .setWebXML("WEB-INF/web.xml")
           ;
   System.out.printf("WebArchive: %s\n", webArchive.toString(true));
   return webArchive;
}

The first part contains some commented out system property settings that turn on different Wildfly-Swarm behaviors that we will look at later. The real work of the createDeployment method is the creation of the webArchive. This is a web application archive abstraction which is built up to contain:

  • the public key of the signer is placed
  • the MySecureWallet and MyJaxrsApp classes
  • the Wildfly-Swarm project-defaults.yml file we looked at a portion of earlier
  • the jwt-roles.properties file used to augment the group to role name mapping we saw earlier
  • A standard web.xml descriptor that specifies that the entire deployment should be secured. This is a current requirement for the Wildfly-Swarm MP-JWT implementation that will be removed in future releases.

The various unit test methods make use of the JAX-RS client API to build the REST web request and handle the responses. The bigSpenderDebitBalanceFail is given in Listing 4:

Listing 4, bigSpenderDebitBalanceFail test method

@RunAsClient
@Test(description = "Verify that jdoe cannot debit amount about the $2500 spendingLimit of Token1.json")
public void bigDebitBalanceFail() throws Exception {
   Reporter.log("Begin bigDebitBalanceFail");
   String token = TokenUtils.generateTokenString("/Token1.json");

   // First get the current balance
   String uri = baseURL.toExternalForm() + "/wallet/balance";
   WebTarget target = ClientBuilder.newClient()
           .target(uri);
   Response response = target.request(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
   JsonObject origBalance = response.readEntity(JsonObject.class);
   Assert.assertTrue(origBalance.containsKey("usd"));
   System.out.println(origBalance.toString());

   // Now try a big debit that is above the $2500 spendingLimit claim
   uri = baseURL.toExternalForm() + "/wallet/debit";
   target = ClientBuilder.newClient()
           .target(uri)
           .queryParam("amount", "3000");
   response = target.request(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
   Assert.assertEquals(response.getStatus(), HttpURLConnection.HTTP_BAD_REQUEST);

   // Now retrieve the balance again to make sure it has not changed
   uri = baseURL.toExternalForm() + "/wallet/balance";
   target = ClientBuilder.newClient()
           .target(uri);
   response = target.request(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
   Assert.assertEquals(response.getStatus(), HttpURLConnection.HTTP_OK);
   JsonObject newBalance = response.readEntity(JsonObject.class);
   Reporter.log(newBalance.toString());
   System.out.println(newBalance.toString());
   Assert.assertEquals(origBalance.getJsonNumber("usd"), newBalance.getJsonNumber("usd"));
}

The method starts by generating the JWT using the TokenUtils class that is from the MP-JWT TCK artifacts. This transforms the Token1.json payload we saw in Listing 3, Token1.json into a signed and encoded string with valid header, along with updated issued at and expiration time claims.

The next step is to query the current balance. It then attempts to make a debit of $3,000 which is above the $2,500 spendingLimit claim value of Token1.json. We looked at this logic earlier. The test validates that this fails with a status code, and then re-queries the balance to validate that the debit did not go through.

The other test methods exercise debiting and crediting the wallet with different role grants and tokens. The description member of each @Test annotation gives the gist of what the test is supposed to do.

Running Tests

You can run the tests by pulling the project into your favorite IDE and making use of it’s TestNg integration, or by using maven test from a command line. To run the full set of test you would issue mvn test:

[eclipse-newsletter-sep-2017 504]$ mvn test
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building MicroProfile JWT Auth WFSwarm Example 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
...
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.813 s - in org.eclipse.microprofile.test.jwt.MySecureWalletTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.525 s
[INFO] Finished at: 2017-09-07T13:10:13-07:00
[INFO] Final Memory: 42M/455M
[INFO] ------------------------------------------------------------------------
[eclipse-newsletter-sep-2017 505]$

To see the gory details of the Wildfly-Swarm implementation behavior, incuding the CDI extension processing of the injection sites, the MP-JWT token handling, etc., use the -Dswarm.logging=DEBUG argument:

[eclipse-newsletter-sep-2017 505]$ mvn test -Dswarm.logging=DEBUG
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building MicroProfile JWT Auth WFSwarm Example 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] 
...
[org.wildfly.swarm.mpjwtauth.deployment.auth.cdi.MPJWTExtension] (MSC service thread 1-7) MPJWTExtension(), added JWTPrincipalProducer
...
[org.wildfly.swarm.mpjwtauth.deployment.auth.cdi.MPJWTExtension] (Weld Thread Pool -- 2) pipRaw: [BackedAnnotatedField] @Inject @Claim private org.eclipse.microprofile.test.jwt.MySecureWallet.warningLimitInstance
2017-09-07 13:14:10,786 DEBUG [org.wildfly.swarm.mpjwtauth.deployment.auth.cdi.MPJWTExtension] (Weld Thread Pool -- 2) pip: [BackedAnnotatedField] @Inject @Claim private org.eclipse.microprofile.test.jwt.MySecureWallet.warningLimitInstance
2017-09-07 13:14:10,786 DEBUG [org.wildfly.swarm.mpjwtauth.deployment.auth.cdi.MPJWTExtension] (Weld Thread Pool -- 2) Checking Provider Claim(warningLimit), ip: [BackedAnnotatedField] @Inject @Claim private org.eclipse.microprofile.test.jwt.MySecureWallet.warningLimitInstance
2017-09-07 13:14:10,786 DEBUG 
...
2017-09-07 13:14:12,470 DEBUG [io.undertow.request.security] (default task-45) Attempting to authenticate HttpServerExchange{ GET /wallet/debit request {Accept=[application/json], Connection=[Keep-Alive], Authorization=[Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIyNDQwMDMyMCIsImF1ZCI6IndhbGxldCIsInVwbiI6Impkb2VAZXhhbXBsZS5jb20iLCJzcGVuZGluZ0xpbWl0IjoyNTAwLCJhdXRoX3RpbWUiOjE1MDQ4MTUyNTIsImlzcyI6Imh0dHBzOlwvXC9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJncm91cHMiOlsiVmlld0JhbGFuY2UiLCJEZWJ0b3IiLCJDcmVkaXRvciJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqZG9lIiwid2FybmluZ0xpbWl0Ijo1MDAwMCwiZXhwIjoxNTA0ODE1NTUyLCJpYXQiOjE1MDQ4MTUyNTIsImp0aSI6ImEtMTIzIn0.damWAS3kRobtORXO3INFOMGy8AXKg4FLVdwR-vJaCf47Cesr7kokp8y1VLc2GLCJnrB68dcLQ05qAYeYGMKnQuMNHVoB8aL1gpeJz_abJx72UrsAXrPcWAx6fVtKjyGF3GQ4onEvKUwo0puR-rbeijPyDqUcnUibfP91_2Wo4F72GK1fyqJKlLTwsUsqYI9OizVR1f3C6wBdGzsOSm50e-FpqMZLI6A4vzOrPzV1RaIa7wpAi-oGa2Xr1-0JYNUqpOjzjQSrYY_urv3NfX7lRU3i34wb02ixBxi4cgL4qkwyFhr8s9HPrg8U-zqYSYspWMmjCYLsuwC4_QLtgG-x_g], User-Agent=[Apache-HttpClient/4.5.2 (Java/1.8.0_121)], Host=[localhost:8080]} response {Expires=[0], Cache-Control=[no-cache, no-store, must-revalidate], Pragma=[no-cache]}}, authentication required: true
2017-09-07 13:14:12,470 DEBUG [org.wildfly.extension.undertow] (default task-45) validateRequest for layer [HttpServlet] and applicationContextIdentifier [default-host ]
2017-09-07 13:14:12,472 DEBUG [io.undertow.request.security] (default task-45) Authenticated as jdoe@example.com, roles [Debtor, ViewBalance, BigSpender, Creditor]
2017-09-07 13:14:12,472 DEBUG [io.undertow.request.security] (default task-45) 

You can run an individual test by passing a -Dtest=... argument like:

[eclipse-newsletter-sep-2017 506]$ mvn -Dtest=org.eclipse.microprofile.test.jwt.MySecureWalletTest#bigDebitBalanceFail test
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building MicroProfile JWT Auth WFSwarm Example 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
{"usd":100000.0000,"bitcoin":22.0361,"ethereum":304.8780}
{"usd":100000.0000,"bitcoin":22.0361,"ethereum":304.8780}
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.581 s - in org.eclipse.microprofile.test.jwt.MySecureWalletTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Future Directions

In future versions of the API we would like to address service specific role and group claims. The "resource_access" claim originally targeted for the 1.0 release of the specification has been postponed as addition work to determine the format of the claim key as well as how these claims would be surfaced through the standard Java EE APIs needs more work than the 1.0 release timeline will allow.

For reference, a somewhat related extension to the OAuth 2.0 spec Resource Indicators for OAuth 2.0 has not gained much traction. The key point it makes that seems relevant to our investigation is that the service specific grants most likely need to be specified using URIs for the service endpoints. How these endpoints map to deployment virtual hosts and partial, wildcard, etc. URIs needs to be determined.

Additional items include:

  • are the standard definition of some type of JsonWebToken factory
  • Integration with the MicroProfile Config specification for the configuration of the whitelisted issuers and associated public keys for validation of tokens.
  • Integration with the MicroProfile Config specification to standardize things like group to role mapping, and other security configurations that are currently implementation specific.
  • Support for Java EE 8 JSR-375 security interfaces

Resources

  • The Eclipse MicroProfile JWT RBAC Repository contains the API, specification and TCK.
  • The article example code

MP-JWT Implementations

This is a list of current implementations of the MP-JWT feature that are either underway or being planned:

About the Author

Scott Stark

Scott Stark
Red Hat