What’s new in Jakarta Security 3?

Despite the version number 3, Jakarta Security 3 is the first real update of Jakarta Security since it was introduced as Java EE Security in Java EE 8.

In this article we’ll take a look at what new things have been added. We’ll first take a look at the user facing umbrella API, which is Jakarta Security itself, and then take a look at the two underlying SPIs it depends on; Jakarta Authentication and Jakarta Authorization.

OpenID Connect

The signature addition to Jakarta Security 3 is the new OpenID Connect authentication mechanism, contributed by Payara’s Lead Developer Rudy De Busscher and Principal Engineer Gaurav Gupta.

OpenID Connect joins the existing Basic, Form and Custom Form authentication mechanisms. The plan to also gain parity to Servlet by adding Jakarta Security versions of the Client-Cert and Digest authentication mechanisms unfortunately failed, as simply nobody picked up the work for that. As Jakarta Security is now mostly a volunteer driven OSS project, that’s of-course how things go; we can plan whatever we want, but are ultimately dependent on volunteers picking up things.

OpenID Connect itself is a mechanism where a third party server takes care of the actual authentication of an end-user, and the result of this authentication is then communicated back to our server. “Our server” here is the one that runs Jakarta Security to secure our web application. Since we rely on a third party server, that remote OpenID Connect server is called a “relying party” in the OpenID Connect terminology.

This is depicted in the following slightly adjusted diagram from the OpenID Connect website:

    +--------+                                                       +--------+
    |        |                                                       |        |
    |        |---------------(1) Authentication Request------------->|        |
    |        |                                                       |        |
    |        |       +--------+                                      |        |
    |        |       |  End-  |<--(2) Authenticates the End-User---->|        |
    |   RP   |       |  User  |                                      |   OP   |
    |        |       +--------+                                      |        |
    |        |                                                       |        |
    |        |<---------(3) Returns Authorization code---------------|        |
    |        |                                                       |        |
    |        |---------(3b)                                          |        |
    |        |           | Redirect to original resource (if any)    |        |
    |        |<----------+                                           |        |
    |        |                                                       |        |
    |        |------------------------------------------------------>|        |
    |        |   (4) Request to TokenEndpoint for Access / Id Token  |        |
    | OpenID |<------------------------------------------------------| OpenID |
    | Connect|                                                       | Connect|
    | Client | ----------------------------------------------------->|Provider|
    |        |   (5) Fetch JWKS to validate ID Token                 |        |
    |        |<------------------------------------------------------|        |
    |        |                                                       |        |
    |        |------------------------------------------------------>|        |
    |        |   (6) Request to UserInfoEndpoint for End-User Claims |        |
    |        |<------------------------------------------------------|        |
    |        |                                                       |        |
    +--------+                                                       +--------+  
See openid.net/specs/openid-connect-core-1_0

RP, or Relying Party, is the Jakarta EE server running our web application. OP, or the OpenID Connect Provider, is the remote server doing authentication. This can be another server that we run ourselves, or, more common, it’s a public service such as Google, Facebook, Twitter, etc.

We can use this authentication mechanism in our own applications via the new “@OpenIdAuthenticationMechanismDefinition” annotation. The following gives an example:

@OpenIdAuthenticationMechanismDefinition(
  
    providerURI =  "http://localhost:8081/openid-connect-server-webapp",

    clientId =     "client",

    clientSecret = "secret",
   
    redirectURI =  "${baseURL}/Callback",

    redirectToOriginalResource = true
)

There are many more attributes that can be configured, but the above is a typical minimal configuration. Let’s quickly take a look at what the various attributes do.

The “providerURI” specifies the location of the OpenID Provider. This is where the end-user is redirected to when logging into our application. The clientId and clientSecret are essentially the username/password to identify ourselves to the OpenID Provider. Note that when the user is redirect only the clientId is put into the redirect URL. The clientSecret is used when we do our secure server to server communication which does not involve the end-user. Also note that in Jakarta Security annotation attributes can contain Jakarta Expression Language, and in practice the clientId and clientSecret would not be put in constants in the code this way.

The redirectURI is the location the end-user is redirected back to after being authenticated. A special placeholder ${baseURL} is used here, which resolves to the actual URL our application is deployed to.

Finally we have the redirectToOriginalResource which makes sure the end-user is redirected back to the original resource (page, path) for the situation where authentication was automatically triggered when a protected resources was accessed. This works in the same way as the well-known FORM authentication mechanism works. When set to false the end-user will stay at the resource behind the redirectURI, which obviously has to exist then. If set to true, the authentication mechanism monitors it, and there doesn’t have to be an actual Servlet or Filter mapped to it.

The following shows this annotation in context on a Servlet:

@OpenIdAuthenticationMechanismDefinition(
    providerURI =  "http://localhost:8081/openid-connect-server-webapp",
    clientId =     "client",
    clientSecret = "secret",
    redirectURI =  "${baseURL}/Callback",
    redirectToOriginalResource = true
)
@WebServlet("/protectedServlet")
@DeclareRoles({ "foo", "bar", "kaz" })
@ServletSecurity(@HttpConstraint(rolesAllowed = "foo"))
public class ProtectedServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    public void doGet(
        HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
        response.getWriter().write("This is a protected servlet \n");
    }

}
In Jakarta Security we typically combine an authentication mechanism with an identity store, which is the entity that validates the credential provided by the end-user. For OpenID Connect that end-user credential is validated by the remote OpenID Connect provider of course. We do have to validate a token coming back from the provider, but that is done internally by the OpenID Connect authentication mechanism (it does uses an identity store for this, but an internal one).

However, a public OpenID Provider typically has no knowledge of groups an end-user has in our application, so we do have to provide an identity store for exactly that purpose. This is basically the same thing as we often have to do for client-cert authentication, as the certificates don’t contain any groups either. The following gives an example of such store:

@ApplicationScoped
public class AuthorizationIdentityStore implements IdentityStore {

    private Map<String, Set<String>> authorization = 
        Map.of("user", Set.of("foo", "bar"));

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

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult result) {
        return authorization.get(result.getCallerPrincipal().getName());
    }

}
In the example store above we map an end-user called “user” to the groups “foo” and “bar”. This identity store will be called together with the internal OpenID Connect identity store, and the job our one here is to provide just the groups.

These two classes together can be packaged up and constitute a full application that we can use to test. It’s available as a Maven project here: app-openid3

Small API enhancements

Next to the ticket feature OpenID Connect, a number of small API enhancements have been added:

CallerPrincipal Serializable

The native Principal type that Jakarta Security uses to denote the caller principal, was not serializable in the first versions. This caused various problems when this principal was stored in an HTTP session, and some kind of fail-over or clustering was used. It’s now Serializable:

/**
 * Principal that represents the caller principal associated with the invocation 
 * being processed by the container (e.g. the current HTTP request).
 */
public class CallerPrincipal implements Principal, Serializable {

Dynamically adding interceptor to a build-in CDI bean

Jakarta Security provides a number of interceptors that add functionality to a bean, mostly beans that are authentication mechanisms. Those are easy to add to one’s own beans, but take somewhat more work to apply to one of the authentication mechanisms that are build in to Jakarta Security.

Two of the artefacts that needed to be created for this to work were a wrapper for the HttpAuthenticationMechanism type, and an annotation literal for the interceptor that we wanted to add dynamically.

This task has been made a little bit easier in Jakarta Security 3, where all Interceptors now have default annotation literals, and the HttpAuthenticationMechanismWrapper type is provided by the API now.

For example:

@Inherited
@InterceptorBinding
@Retention(RUNTIME)
@Target(TYPE)
public @interface AutoApplySession {

    /**
     * Supports inline instantiation of the AutoApplySession annotation.
     *
     * @since 3.0
     */
    public static final class Literal extends AnnotationLiteral<AutoApplySession>
        implements AutoApplySession {
        private static final long serialVersionUID = 1L;

        /**
         * Instance of the {@link AutoApplySession} Interceptor Binding.
         */
        public static final Literal INSTANCE = new Literal();
    }
}

Jakarta Authentication

Jakarta Authentication is the underlying SPI on which Jakarta Security depends. Enhancements here mostly benefit library vendors, although some advanced users can opt to direct use it as well.

Register ServerAuthModule

The end user of Jakarta Authentication, as well as integrators such as Jakarta Security implementations almost always just care about registering a ServerAuthModule. Yet the AuthConfigFactory only accepts an AuthConfigProvider, which is essentially a “wrapper-wrapper-wrapper-wrapper” of a ServerAuthModule to the end user. A new method has been added to the AuthConfigFactory to only register a ServerAuthModule.

A ServerAuthModule is typically installed in a servlet listener. The following is an example:

@WebListener
public class SamAutoRegistrationListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        AuthConfigFactory
            .getFactory()
            .registerServerAuthModule(
                new TestServerAuthModule(),
                sce.getServletContext());
    }

}

Add missing generics to API

Jakarta Authentication has curiously been at Java SE 1.4 even in Jakarta EE 9.1, which officially targets Java SE 8 and 11. This specifically meant a lot of generics were missing everywhere in the API. These have now been added. For example:

public interface ServerAuthModule extends ServerAuth {

    void initialize(
        MessagePolicy requestPolicy, MessagePolicy responsePolicy, 
        CallbackHandler handler, Map<String, Object> options) 
        throws AuthException;

    Class<?>[] getSupportedMessageTypes();
}

Add default methods

A ServerAuthModule requires methods for “secureResponse” and “cleanSubject” to be implemented, but by far not all ServerAuthModules need to do something there. For these methods defaults have been added, so implementations that don’t need those can be a little less verbose. The interface now looks as follows:
public interface ServerAuth {

    AuthStatus validateRequest(
        MessageInfo messageInfo, Subject clientSubject, 
        Subject serviceSubject) throws AuthException;

    default AuthStatus secureResponse(
        MessageInfo messageInfo, Subject serviceSubject) 
        throws AuthException {
        return AuthStatus.SEND_SUCCESS;
    }

    default void cleanSubject(
        MessageInfo messageInfo, Subject subject) 
        throws AuthException {
    }
}

Add constructor taking cause to AuthException

Jakarta Authentication being at Java SE 1.4 level meant its AuthException didn’t make use of setting the exception cause that was added in Java SE 5.

Throwing an exception from Jakarta Authentication code was therefor more than a little verbose:

throw (AuthException) new AuthException().initCause(e);
New constructors have been added now taking a cause, so that we can now do:
throw new AuthException(e);

Distinguish between invocation at start of request and invocation following authenticate()

In the Servlet Container Profile of Jakarta Authentication a ServerAuthModule can be called by the container at the start of a request (before Filters and Servlets are invoked) or following a call to HttpServletRequest.authenticate(). For a ServerAuthModule there’s no way to distinguish between those two cases, which is sometimes needed for more advanced interactions.

A ServerAuthModule can now check this by looking at the `jakarta.servlet.http.isAuthenticationRequest` key in the message info map.

Jakarta Authorization

Jakarta Authorization is another underlying SPI on which Jakarta Security depends. Enhancements here too mostly benefit library vendors, although some advanced users can opt to direct use it as well.

Add getPolicyConfiguration methods without state requirement

The PolicyConfigurationFactory in Jakarta Authorization has methods to retrieve a policy configuration instance, which hold a collection of permissions which are used for authorization decisions. A Policy (authorization module) can however not easily use these, as all the existing methods have required side effects. In practice such Policy therefor needs to resort to implementation specific ways, often strongly coupling the PolicyConfigurationFactory and Policy. For the new release methods have been added to get that PolicyConfiguration directly without any side effects;
     public abstract PolicyConfiguration getPolicyConfiguration(String contextID);
     public abstract PolicyConfiguration getPolicyConfiguration();
The first variant can be used when the Policy already has the contextID (an identifier for the application), while the second variant is a convenient method that returns the PolicyConfiguration for the contextID that’s set on the calling thread.

Add methods to PolicyConfiguation to read permissions

The PolicyConfiguration as mentioned above stores the permissions, but curiously enough didn’t contain methods before to read these permissions back. A Policy always needed to resort to implementation specific methods to obtain these permissions. For instance, in old versions of GlassFish the PolicyConfiguration would write its permissions to a policy file on disk first, and then the Policy would read that file back. Now finally some methods have been added to directly read back the permissions:
     Map<String, PermissionCollection> getPerRolePermissions();
     PermissionCollection getUncheckedPermissions();
     PermissionCollection getExcludedPermissions();

Generic return value for getContext

Jakarta Authorization has a PolicyContext object from which instances of various types can be obtained, most importantly the Subject. The signature of this method returned an Object before, so that a cast was always needed. In the new version this has been changed to a generic return value: public static T getContext(String key) throws PolicyContextException So for example previously one did:
     Subject subject = 
         (Subject) PolicyContext.getContext("javax.security.auth.Subject.container");
Which can now be:
     Subject subject = PolicyContext.getContext(“javax.security.auth.Subject.container");

Final thoughts

The amount of changes for Jakarta Security 3 is smaller than planned, but the big ticket feature OpenID Connect is very welcome. It was planned for the initial release, and some implementations had started, but ultimately didn’t make it in back then. The changes in the lower level SPIs are small, but some of them quite important. Looking forward, the next version of Jakarta Security should focus more on the authorization topic. Authorization modules are still somewhat of an obscure thing in the current API, which is shame, as it's a very powerful concept. This update has set the stage for a more accessible future API there.

Arjan Tijms

Comments

  1. hi, nice post, I loved .. is it possible to use this api (jakarta.security) in tomcat?

    ReplyDelete

Post a Comment

Popular posts from this blog

Implementing container authentication in Java EE with JASPIC

Jakarta EE Survey 2022

Counting the rows returned from a JPA query