Last week we published the post, “ MicroProfile JWT ” by @JLouisMonteir o 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 RBACFor 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.
@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 org.eclipse.microprofile.jwt.Claim;@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.
@Path("movies")@ApplicationScoped
public class MovieAppResource {
@Inject
private Person person;
}
import org.eclipse.microprofile.jwt.Claim;
@RequestScopedpublic 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;
}
import org.eclipse.microprofile.jwt.Claim;
@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.core.SecurityContext;@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.
@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