1. Overview

The Jakarta EE 8 Security API is the new standard and a portable way of handling security concerns in Java containers.

In this article, we’ll look at the three core features of the API:

  1. HTTP Authentication Mechanism
  2. Identity Store
  3. Security Context

We’ll first understand how to configure the provided implementations and then how to implement a custom one.

2. Maven Dependencies

To set up the Jakarta EE 8 Security API, we need either a server-provided implementation or an explicit one.

2.1. Using the Server Implementation

Jakarta EE 8 compliant servers already provide an implementation for the Jakarta EE 8 Security API, and therefore we need only the Jakarta EE Web Profile API Maven artifact:

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0.1</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2. Using an Explicit Implementation

First, we specify the Maven artifact for the Jakarta EE 8 Security API:

<dependencies>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

And then, we’ll add an implementation, for example, Soteria – the reference implementation:

<dependencies>
    <dependency>
        <groupId>org.glassfish.soteria</groupId>
        <artifactId>javax.security.enterprise</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

3. HTTP Authentication Mechanism

Prior to Jakarta EE 8, we configured Authentication mechanisms declaratively through the web.xml file.

In this version, the Jakarta EE 8 Security API has designed the new HttpAuthenticationMechanism interface as a replacement. Therefore, web applications can now configure Authentication mechanisms by providing implementations of this interface.

Fortunately, the container already provides an implementation for each of the three authentication methods defined by the Servlet specification: Basic HTTP authentication, form-based authentication, and custom form-based authentication.

It also provides an annotation to trigger each implementation:

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Basic HTTP Authentication

As mentioned above, a web application can configure the Basic HTTP Authentication just by using the @BasicAuthenticationMechanismDefinition annotation on a CDI bean:

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

At this point, the Servlet container searches and instantiates the provided implementation of the HttpAuthenticationMechanism interface.

Upon receipt of an unauthorized request, the container challenges the client to provide suitable authentication information via the WWW-Authenticate response header.

WWW-Authenticate: Basic realm="userRealm"

The client then sends the username and password, separated by a colon “:” and encoded in Base64, via the Authorization request header:

//user=baeldung, password=baeldung
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

Note that the dialog presented for providing credentials is coming from the browser and not from the server.

3.2. Form-based HTTP Authentication

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication as defined by the Servlet specification.

Then we have the option to specify the login and error pages or use the default reasonable ones /login and /login-error:

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

As a result of invoking loginPage, the server should send the form to the client:

<form action="j_security_check" method="post">
    <input name="j_username" type="text"/>
    <input name="j_password" type="password"/>
    <input type="submit">
</form>

The client then should send the form to a pre-defined backing authentication process provided by the container.

3.3. Custom Form-based HTTP Authentication

A web application can trigger the custom form-based authentication implementation by using the annotation @CustomFormAuthenticationMechanismDefinition:

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

But unlike the default form-based authentication, we’re configuring a custom login page and invoking the SecurityContext.authenticate() method as a backing authentication process.

Let’s have a look at the backing LoginBean as well, which contains the login logic:

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }
     
    // ...
}

As a result of invoking the custom login.xhtml page, the client submits the received form to the LoginBean’s login() method:

//...
<input type="submit" value="Login" jsf:action="#{loginBean.login}"/>

3.4. Custom Authentication Mechanism

The HttpAuthenticationMechanism interface defines three methods. The most important is the validateRequest(), which we must provide an implementation.

The default behaviour for the other two methods, secureResponse() and cleanSubject(), is sufficient in most cases.

Let’s have a look at an example implementation:

@ApplicationScoped
public class CustomAuthentication 
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response, 
      HttpMessageContext httpMsgContext) 
      throws AuthenticationException {
 
        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

Here, the implementation provides the business logic of the validation process, but in practice, it’s recommended to delegate to the IdentityStore through the IdentityStoreHandler by invoking validate.

We’ve also annotated the implementation with @ApplicationScoped annotation as we need to make it CDI-enabled.

After a valid verification of the credential and an eventual retrieval of user roles, the implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Enforcing Servlet Security

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET", 
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(     
      value = "POST", 
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

This annotation has two attributes – httpMethodConstraints and valuehttpMethodConstraints is used to specify one or more constraints, each one representing an access control to an HTTP method by a list of allowed roles.

The container will then check, for every URL pattern and HTTP method, if the connected user has a suitable role for accessing the resource.

4. Identity Store

This feature is abstracted by the IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership. In other words, it can provide capabilities for authentication, authorization or both*.*

IdentityStore is intended and encouraged to be used by the HttpAuthenticationMecanism through a called IdentityStoreHandler interface. A default implementation of the IdentityStoreHandler is provided by the Servlet container.

An application can provide its implementation of the IdentityStore or uses one of the two built-in implementations provided by the container for Database and LDAP.

4.1. Built-in Identity Stores

The Jakarta EE compliant server should provide implementations for the two Identity Stores: Database and LDAP.

The database IdentityStore implementation is initialized by passing a configuration data to the @DataBaseIdentityStoreDefinition annotation:

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

As configuration data, we need a JNDI data source to an external database, two JDBC statements for checking the caller and his groups and finally, a priority parameter which is used in case of multiples store are configured.

IdentityStore with high priority is processed later by the IdentityStoreHandler.

Like the database, LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition by passing configuration data:

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=baeldung,dc=com",
  groupSearchBase = "ou=group,dc=baeldung,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

Here we need the URL of an external LDAP server, how to search the caller in the LDAP directory, and how to retrieve his groups.

4.2. Implementing a Custom IdentityStore

The IdentityStore interface defines four default methods:

default CredentialValidationResult validate(
  Credential credential)
default Set<String> getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set<ValidationType> validationTypes()

The priority() method returns a value for the order of iteration; this implementation is processed by IdentityStoreHandler. An IdentityStore with lower priority is treated first.

By default, an IdentityStore processes both credentials validation (ValidationType.VALIDATE) and group retrieval(ValidationType.PROVIDE_GROUPS). We can override this behaviour so that it can provide only one capability.

Thus, we can configure the IdentityStore to be used only for credentials validation:

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

In this case, we should provide an implementation for the validate() method:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate( 
      UsernamePasswordCredential credential) {
 
        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

Or we can choose to configure the IdentityStore so that it can be used only for group retrieval:

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

We should then provide an implementation for the getCallerGroups() methods:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

Because IdentityStoreHandler expects the implementation to be a CDI bean, we decorate it with ApplicationScoped annotation.

5. Security Context API

The Jakarta EE 8 Security API provides an access point to programmatic security through the SecurityContext interface. It’s an alternative when the declarative security model enforced by the container isn’t sufficient.

A default implementation of the SecurityContext interface should be provided at runtime as a CDI bean, and therefore we need to inject it:

@Inject
SecurityContext securityContext;

At this point, we can authenticate the user, retrieve an authenticated one, check his role membership and grant or deny access to web resources through the five available methods.

5.1. Retrieving Caller Data

In previous versions of Jakarta EE, we’d retrieve the Principal or check the role membership differently in each container.

While we use the getUserPrincipal() and isUserInRole() methods of the HttpServletRequest in a servlet container, a similar methods getCallerPrincipal() and isCallerInRole() methods of the EJBContext are used in EJB Container.

The new Jakarta EE 8 Security API has standardized this by providing a similar method through the SecurityContext interface:

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
<T extends Principal> Set<T> getPrincipalsByType(Class<T> type);

The getCallerPrincipal() method returns a container-specific representation of the authenticated caller, while the getPrincipalsByType() method retrieves all principals of a given type.

It can be useful in case the application-specific caller is different from the container one.

5.2. Testing for Web Resource Access

First, we need to configure a protected resource:

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

And then, to check access to this protected resource, we should invoke the hasAccessToWebResource() method:

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

In this case, the method returns true if the user is in role USER_ROLE.

5.3. Authenticating the Caller Programmatically

An application can programmatically trigger the authentication process by invoking authenticate():

AuthenticationStatus authenticate(
  HttpServletRequest request, 
  HttpServletResponse response,
  AuthenticationParameters parameters);

The container is then notified and will, in turn, invoke the authentication mechanism configured for the application. AuthenticationParameters parameter provides a credential to HttpAuthenticationMechanism:

withParams().credential(credential)

The SUCCESS and SEND_FAILURE values of the AuthenticationStatus design a successful and failed authentication, while SEND_CONTINUE  signals an in-progress status of the authentication process.

6. Running the Examples

For highlighting these examples, we’ve used the latest development build of the Open Liberty Server, which supports Jakarta EE 8. This is downloaded and installed thanks to the liberty-maven-plugin, which will fetch all dependencies that will need to start the server.

To run the examples, just access the corresponding module and invoke this command:

mvn clean package liberty:run

As a result, Maven will download the server, build, deploy, and run the application.

7. Conclusion

In this article, we covered the configuration and implementation of the main features of the new Jakarta EE 8 Security API.

First, we started by showing how to configure the default built-in authentication mechanisms and how to implement a custom one. Later, we saw how to configure the built-in Identity Store and how to implement a custom one. And finally, we saw how to call the methods of the SecurityContext.

As always, the code examples for this article are available on GitHub.