Skip to main content

While working on Jakarta EE 10 certification (See announcement Apache Tomee Jakarta EE certified after 10 years, Apache TomEE implemented Jakarta Security specification. 

Currently, there is only one implementation used in Glassfish and used by all the other vendors for Jakarta Security. In TomEE, we decided to create an alternative to bring some diversity, and have an Apache implementation.

What is Jakarta Security?

Jakarta Security defines a standard for creating secure Jakarta EE applications in modern application paradigms. It defines an overarching (end-user targeted) Security API for Jakarta EE Applications.

Jakarta Security builds on the lower level Security SPIs defined by Jakarta Authentication and Jakarta Authorization, which are both not end-end targeted.

What are we going to do?

This blog will show how to leverage Jakarta Security to implement authentication and authorization on a simple JAX RS application using Tomcat tomcat-users.xml file.

Why tomcat-users.xml?

Tomcat has created this simple file to store users and roles. It is commonly used in development or simple applications, usually using Tomcat realms.

In the Apache implementation of Jakarta Security, we decided to support “out of the box” tomcat-users.xml as a built-in identity store.

What is an identity store?

An identity store is a database or a directory (store) of identity information about a population of users that includes an application’s callers.

In essence, an identity store contains all information such as caller name, groups or roles, and required information to validate a caller’s credentials.

Example

An example has been created and committed to the Apache TomEE repository under the Examples section (https://github.com/apache/tomee/tree/master/examples/security-tomcat-user-identitystore). This is a self contained example you can check out and run on your laptop. It should contain all information and the minimum required configuration and code.

Configuration

In terms of configuration, there are a couple of important things to do.

1/ define some users with roles in tomcat-users.xml 

<tomcat-users>
 <user name="tomcat" password="tomcat" roles="tomcat"/>
 <user name="user" password="user" roles="user"/>

 <user name="tom" password="secret1" roles="admin,manager"/>
 <user name="emma" password="secret2" roles="admin,employee"/>
 <user name="bob" password="secret3" roles="admin"/>
</tomcat-users>

2/ Protect your JAX RS resource

<web-app
 xmlns="http://xmlns.jcp.org/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
 version="3.1"
>
 <!-- Security constraints  -->
 <security-constraint>
   <web-resource-collection>
     <web-resource-name>Protected admin resource/url</web-resource-name>
     <url-pattern>/api/movies/*</url-pattern>
     <http-method-omission>GET</http-method-omission>
   </web-resource-collection>
   <auth-constraint>
     <role-name>admin</role-name>
   </auth-constraint>
 </security-constraint>
</web-app>

Show me the code

The code is rather simple and uses plain JAX RS APIs. The only thing to remember is to define the identity store and the authentication mechanism, both with an annotation such as: 

@Path("/movies")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@TomcatUserIdentityStoreDefinition
@BasicAuthenticationMechanismDefinition
@ApplicationScoped
public class MovieAdminResource {

   private static final Logger LOGGER = Logger.getLogger(MovieAdminResource.class.getName());

   @Inject
   private MovieStore store;

   // JAXRS security context also wired with Jakarta Security
   @Context
   private javax.ws.rs.core.SecurityContext securityContext;

   @POST
   public Movie addMovie(final Movie newMovie) {
       LOGGER.info(getUserName() + " adding new movie " + newMovie);
       return store.addMovie(newMovie);
   }

   // See source file for full content
   private String getUserName() {
       if (securityContext.getUserPrincipal() != null) {
           return String.format("%s[admin=%s]",
                                securityContext.getUserPrincipal().getName(),
                                securityContext.isUserInRole("admin"));
       }
       return null;
   }
}
  • Selecting the identity store: as explained above, TomEE implementation supports “out of the box” the required LDAP and Datasource identity store using standard annotations (example coming soon). TomEE also created a new annotation to add support for `tomcat-users.xml` from Tomcat. This is done using @TomcatUserIdentityStoreDefinition.
  • Selecting the authentication mechanism: specification requires Basic, Form and Custom Form to be supported. In this example, we used @BasicAuthenticationMechanismDefinition (more examples coming soon).

Testing with TomEE serverless

In this example, we decided to use TomEE serverless to write the tests.

public class MovieResourceTest {

   private static URI serverURI;

   @BeforeClass
   public static void setup() {
       // Add any classes you need to an Archive
       // or add them to a jar via any means
       final Archive classes = Archive.archive()
               .add(Api.class)
               .add(Movie.class)
               .add(MovieStore.class)
               .add(MovieResource.class)
               .add(MovieAdminResource.class);

       // Place the classes where you would want
       // them in a Tomcat install
       final Server server = Server.builder()
               // This effectively creates a webapp called ROOT
               .add("webapps/ROOT/WEB-INF/classes", classes)
               .add("webapps/ROOT/WEB-INF/web.xml", new File("src/main/webapp/WEB-INF/web.xml"))
               .add("conf/tomcat-users.xml", new File("src/main/resources/conf/tomcat-users.xml"))
               .build();

       serverURI = server.getURI();
   }

   @Test
   public void getAllMovies() {
       final WebTarget target = ClientBuilder.newClient().target(serverURI);

       final Movie[] movies = target.path("/api/movies").request().get(Movie[].class);

       assertEquals(6, movies.length);

       final Movie movie = movies[1];
       assertEquals("Todd Phillips", movie.getDirector());
       assertEquals("Starsky & Hutch", movie.getTitle());
       assertEquals("Action", movie.getGenre());
       assertEquals(2004, movie.getYear());
       assertEquals(2, movie.getId());
   }

   @Test
   public void addMovieAdmin() {
       final WebTarget target = ClientBuilder.newClient()
                                             .target(serverURI)
                                             .register(new BasicAuthFilter("tom", "secret1"));

       final Movie movie = new Movie("Shanghai Noon", "Tom Dey", "Comedy", 7, 2000);

       final Movie posted = target.path("/api/movies").request()
               .post(entity(movie, MediaType.APPLICATION_JSON))
               .readEntity(Movie.class);

       assertEquals("Tom Dey", posted.getDirector());
       assertEquals("Shanghai Noon", posted.getTitle());
       assertEquals("Comedy", posted.getGenre());
       assertEquals(2000, posted.getYear());
       assertEquals(7, posted.getId());
   }

}

The #setup() method is used to create the webapp and start TomEE serverless.

In the tests, you may notice we are using JAX RS WebClient with a Client filter to automatically compute and add the Authorization header to the request.

public class BasicAuthFilter implements ClientRequestFilter {
   private final String username;
   private final String password;

   public BasicAuthFilter(final String username, final String password) {
       this.username = username;
       this.password = password;
   }

   @Override
   public void filter(final ClientRequestContext requestContext) throws IOException {
       requestContext.getHeaders()
                     .add(AUTHORIZATION,
                          "Basic " + new String(Base64.getEncoder().encode((username + ":" + password).getBytes())));
   }
}
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