Jakarta Security and Tomcat 10

Jakarta Security was introduceed as Java EE Security in Java EE 8. It facilitates portable application security that fully integrates with container security. This means that an application can provide an authentication mechanism, say for OATH2 or Auth0 and that mechanism is treated just like build-in container mechanisms like FORM. All existing security code, such as the container determining access to a URL based on web.xml constraints, and things like @RolesAllowed and HttpServletRequest.isUserInRole automatically work as expected.

One of the compatible implementations of Jakarta Security is Soteria. Soteria has been designed as a standalone library, that can be integrated with multiple servers. It depends on CDI, and the lower level SPIs Jakarta Authentication and Jakarta Authorization.

Soteria worked on Tomcat before, but there were some issues. For one, when adding a CDI implementation like Weld to Tomcat, the BeanManager ends up in the JNDI location java:comp/env/BeanManager, while the specification defined location should be java:comp/BeanManager. The latest version of Soteria now looks at this location too, so no patching is required anymore.

Another issue was that Tomcat implements the servlet container profile of Jakarta Authentication, but not Jakarta Authorization. There are essentially two options to overcome that here:

  1. Add Jakarta Authorization support to Tomcat
  2. Implement an SPI for Soteria to use native Tomcat Authorization code
An independent implementation of Jakarta Authorization is available from the Exousia project. This project recently added integration support specifically for Tomcat, so that we only need to add it as a dependency. To use Jakarta Security in Tomcat, we can create a Maven project with the following dependencies:
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>9.0.0</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.glassfish.soteria</groupId>
    <artifactId>jakarta.security.enterprise</artifactId>
    <version>2.0.1</version>
</dependency>

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>exousia</artifactId>
    <version>1.0</version>
</dependency>

<dependency>
    <groupId>org.jboss.weld.servlet</groupId>
    <artifactId>weld-servlet-shaded</artifactId>
    <version>4.0.0.Final</version>
</dependency>
Additionally, since Tomcat has a read-only JNDI, a file in [war root]/META-INF/context.xml is needed with the following content to make the BeanManager available:
<?xml version='1.0' encoding='utf-8'?>
<Context>
    <Resource 
        name="BeanManager" 
        auth="Container" 
        type="javax.enterprise.inject.spi.BeanManager"
        factory="org.jboss.weld.resources.ManagerObjectFactory" 
    />
</Context>
To test if everything works, put an empty beans.xml file in WEB-INF and a web.xml file with the following content:
<web-app 
    xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
    version="5.0">
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/foo/*</url-pattern>
            <http-method>GET</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>g1</role-name>
        </auth-constraint>
    </security-constraint>
    
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/foox/*</url-pattern>
            <http-method>GET</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>g2</role-name>
        </auth-constraint>
    </security-constraint>
</web-app>
Then add two classes:
@BasicAuthenticationMechanismDefinition(realmName = "realm")
@ServletSecurity(value = @HttpConstraint(rolesAllowed = "g1"))
@WebServlet(urlPatterns = "/SecureServlet")
public class SecureServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Inject
    SecurityContext securityContext;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
        response.getWriter().println(
            "Has access to /foo/bar " + 
            securityContext.hasAccessToWebResource("/foo/bar", "GET"));
        response.getWriter().println(
            "Has access to /foox/bar " + 
            securityContext.hasAccessToWebResource("/foox/bar", "GET"));
    }

}


@ApplicationScoped
public class MyIdentityStore implements IdentityStore {
    public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
        if (usernamePasswordCredential.compareTo("u1", "p1")) {
            return new CredentialValidationResult("u1", new HashSet<>(asList("g1")));
        }

        return INVALID_RESULT;
    }
}

Naming the app "security-test", deploying this to a default installed Tomcat 10 and requesting "http://localhost:8080/security-test/SecureServlet" via a web browser will show the basic authentication dialog from that browser. If we then authenticate with username "u1" and password "p1", we'll get to see the following result:

Has access to /foo/bar true
Has access to /foox/bar false

So what happened here?

Behind the scenes quite a lot. Soteria installed a ServerAuthModule with Tomcat, which uses Weld to find and call the CDI bean installed by @BasicAuthenticationMechanismDefinition. This bean calls the IdentityStore MyIdentityStore, which is the one we defined in our small application and that validates the credentials we submit.

Furthermore, Exousia copied the security constraints that Tomcat collected to a Jakarta Authorization module, which is a store of permissions (aka Permission Store) that among others can be queried by Jakarta Authorization. The default implementation in Soteria of SecurityContext#hasAccessToWebResource indeed results in such a query. In our web.xml file we defined two constraints on URL patterns; /foo/* needing role g1 and /foox/* needing role g2. At the point of making that call, we're in role g1, so asking if we can access /foo/bar results in a true, while asking for access to /foox/bar results in a false. This shows that Jakarta Authentication (Exousia) works correctly on Tomcat and works correctly with Soteria.

There's a small caveat here. Exousia now copies the security constraints from Tomcat, but Tomcat keeps using its own internal repository for authorization decissions. The assumption here is that both Tomcat and Exousia perform the exact same algorithm, but there can of course be small subtle differences. A next step would be to integrate Exousia further in Tomcat by wrapping the Realm and delegating methods like hasUserDataPermission, hasResourcePermission, and hasRole.

Arjan Tijms

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Arjan,

    Is It possible to use Exousia on Tomcat 9 with Soteria v1.0.1 ?

    Thanks a lot

    ReplyDelete
    Replies
    1. With some extra work you could indeed. The previous version of Exousia was still javax and you could relatively easily port back the latest Tomcat addition. Soteria 1.0.1 is javax as well and would need the small change for the bean lookup done in Soteria 2.

      Delete

Post a Comment

Popular posts from this blog

JSF 2.3 released!

Dynamic beans in CDI 2.0

Dynamically adding an interceptor to a build-in CDI bean