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())));
}
}