Last week we published the post, “MicroProfile JWT” by @JLouisMonteiro that covers the basics, standards, pros, and cons involved in implementing stateless security with Microprofile JWT.

In this post, we are going to deep dive into JWT role-based access control on Apache TomEE. Since the release of TomEE 7.1, we now have a JWT specification implementation defined using Microprofile version 1.2. The specification is available in the official release.

Marking a JAX-RS Application as Requiring MP-JWT RBAC

For this, you need to add the annotation @LoginConfig to your existing javax.ws.rs.core.Application subclass. The purpose of the @LoginConfig is to annotate the JAX-RS Application as requiring MicroProfile JWT RBAC. This is the equivalent to the web.xml login-config element.

import javax.ws.rs.core.Application;
import org.eclipse.microprofile.auth.LoginConfig;

@LoginConfig(authMethod = "MP-JWT")
public class ApplicationConfig extends Application {
}

Accessing Claims

We can inject JsonWebToken to denote the currently authenticated caller with @RequestScoped scoping.

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

@Path("movies")
@ApplicationScoped
public class MovieAppResource {
  @Inject
  private JsonWebToken jwtPrincipal;
}

From the current JsonWebToken we can also have an injection of claims using the ClaimValue interface.

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("movies")
@ApplicationScoped
public class MovieAppResource {
  @Inject
  @Claim("email")
  private ClaimValue email;
}

Notice that in the previous example, the MovieAppResource has an ApplicationScope, that’s why we need to have a claim injected using the ClaimValue type. Another way to extend the JWT propagation is by creating a RequestScoped class and use an instance of that class as an attribute of the ApplicationScope resource. The end result provides a cleaner code and better encapsulation.

import javax.ws.rs.Path;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@Path("movies")
@ApplicationScoped
public class MovieAppResource {
  @Inject
  private Person person;
}

import org.eclipse.microprofile.jwt.Claim;

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

@RequestScoped
public class Person {
  @Inject
  @Claim("email")
  private String email;

  public Person() {
  }

  public String getEmail() {
    return email;
  }
}

Following previous JWT propagation approach, we can also map enum attributes for the Person class:

public enum Language {
   SPANISH,
   ENGLISH;
}

@RequestScoped
public class Person {
  @Inject
  @Claim("language")
  private Language language;

  public Person() {
  }

  public Language getLanguage() {
    return language;
  }
}

JAX-RS Container API Integration

A JAX-RS Security method like isUserInRole from SecurityContext returns 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. The getUserPrincipal() method returns an instance of JsonWebToken we previously described.

import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("movies")
@ApplicationScoped
public class MovieAppResource {
  @Context
  private SecurityContext securityContext;
 
  if (!securityContext.isUserInRole("admin")) { //Do fun operations }

  JsonWebToken jwt = (JsonWebToken) securityContext.getUserPrincipal();
}

The MP-JWT supports the expected annotations RolesAllowed, PermitAll and DenyAll described in the JSR-250.

As defined in the MP-JWT specification, any role names used in @RolesAllowed, or equivalent security constraint metadata, that match names in the role names are part of the MP-JWT "groups" claim. The groups of claims is an abstraction base upon the fact that the JWT payload is not limited to store only claims related with roles.

The @RolesAllowed annotation still allows authorization decision wherever the security constraint has been applied.

import javax.ws.rs.Path;
import javax.ws.rs.PUT;
import javax.ws.rs.DELETE;
import javax.ws.rs.Consumes;
import javax.ws.rs.PathParam;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;

@Path("movies")
@ApplicationScoped
public class MovieAppResource {
  @PUT
  @Path("{id}")
  @Consumes("application/json")
  @RolesAllowed("update")
  public Movie method( @PathParam("id") long id, Movie movie) {
    //Do fun operations
    return movie;
  }

  @DELETE
  @Path("{id}")
  @RolesAllowed("delete")
  public void deleteMovie(@PathParam("id") long id) {
    //Do fun operations
    service.deleteMovie(id);
  }
}

Public Key Configuration

Verification of JSON Web Tokens (JWT) passed to the Microservice in HTTP requests at runtime is done with the RSA Public Key corresponding to the RSA Private Key held by the JWT Issuer.

MicroProfile JWT leverages the MicroProfile Config specification to provide a consistent means of passing all supported configuration options via system properties. Any vendor-specific methods of configuration are still valid and shall be considered to override any standard configuration mechanisms.

Here we see the TomEE flavor which provides a runtime alternative for configuring the RSA public key for verification and offers additional settings to configure a window of tolerance for the JWT’s exp claim.

import org.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo;
import org.superbiz.moviefun.utils.TokenUtil;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Optional;

@Dependent
public class MoviesMPJWTConfigurationProvider {

   public static final String ISSUED_BY = "/oauth2/token";

   @Produces
   Optional getOptionalContextInfo() throws Exception {
       JWTAuthContextInfo contextInfo = new JWTAuthContextInfo();

       // todo use MP Config to load the configuration
       contextInfo.setIssuedBy(ISSUED_BY);

       byte[] encodedBytes = TokenUtil.readPublicKey("/publicKey.pem").getEncoded();

       final X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedBytes);
       final KeyFactory kf = KeyFactory.getInstance("RSA");
       final RSAPublicKey pk = (RSAPublicKey) kf.generatePublic(spec);

       contextInfo.setSignerKey(pk);
       contextInfo.setExpGracePeriodSecs(10);

       return Optional.of(contextInfo);
   }

   @Produces
   JWTAuthContextInfo getContextInfo() throws Exception {
       return getOptionalContextInfo().get();
   }
}

You can check an entire application making usage of TomEE 7.1 with JWT support here:
https://github.com/tomitribe/microservice-with-jwt-and-microprofile.git

 

Ivan Junckes Filho

Ivan joins the Tomitribe Product Team with 8+ years of software development experience, including several years at IBM. His core skills include Java EE, REST, SOA, JPA, CDI, JMS, NoSQL databases, Applications Servers such as Apache TomEE and JBoss, Agile plus other Java related technologies.

Cesar Hernandez

Cesar Hernandez

César has experience in the development of Enterprise Java Applications for public and private sectors. He has led implementations of Continuous Integration with Jmeter, SonarQube, Docker, Jenkins, Maven and JBehave. As a software architect he directed application server migrations with legacy systems to take advantage of modern technologies like JPA, CDI, JAX-WS and RESTful services.
CesarHgt

Leave a Reply