Chapter 7. Container and Client Interceptors

7.1. About Container Interceptors

Standard Java EE interceptors, as defined by the JSR 345, Enterprise JavaBeans 3.2 specification, are expected to run after the container has completed security context propagation, transaction management, and other container provided invocation processing. This is a problem if the application must intercept a call before a specific container interceptor is run.

Positioning of the Container Interceptor in the Interceptor Chain

The container interceptors configured for an EJB are guaranteed to be run before the JBoss EAP provided security interceptors, transaction management interceptors, and other server provided interceptors. This allows specific application container interceptors to process or configure relevant context data before the invocation proceeds.

Differences Between the Container Interceptor and the Java EE Interceptor API

Although container interceptors are modeled to be similar to Java EE interceptors, there are some differences in the semantics of the API. For example, it is illegal for container interceptors to invoke the javax.interceptor.InvocationContext.getTarget() method because these interceptors are invoked long before the EJB components are set up or instantiated.

7.2. Create a Container Interceptor Class

Container interceptor classes are simple Plain Old Java Objects (POJOs). They use the @javax.annotation.AroundInvoke to mark the method that is invoked during the invocation on the bean.

The following is an example of a container interceptor class that marks the iAmAround method for invocation:

Container Interceptor Code Example

public class ClassLevelContainerInterceptor {
    @AroundInvoke
    private Object iAmAround(final InvocationContext invocationContext) throws Exception {
        return this.getClass().getName() + " " + invocationContext.proceed();
    }
}
----

For an example of how to configure a jboss-ejb3.xml descriptor file to use a container interceptor class, see Configure a Container Interceptor.

7.3. Configure a Container Interceptor

Container interceptors use the standard Java EE interceptor libraries, meaning they use the same XSD elements that are allowed in ejb-jar.xml file for the 3.2 version of the ejb-jar deployment descriptor. Because they are based on the standard Java EE interceptor libraries, container interceptors may only be configured using deployment descriptors. This was done by design so applications would not require any JBoss specific annotation or other library dependencies. For more information about container interceptors, see About Container Interceptors.

The following procedure describes how to configure a container interceptor.

  1. Create a jboss-ejb3.xml file in the META-INF/ directory of the EJB deployment.
  2. Configure the container interceptor elements in the descriptor file.

    1. Use the urn:container-interceptors:1.0 namespace to specify configuration of container interceptor elements.
    2. Use the <container-interceptors> element to specify the container interceptors.
    3. Use the <interceptor-binding> elements to bind the container interceptor to the EJBs. The interceptors can be bound in any of the following ways:

      • Bind the interceptor to all the EJBs in the deployment using a wildcard (*).
      • Bind the interceptor at the individual bean level using the specific EJB name.
      • Bind the interceptor at the specific method level for the EJBs.

        Note

        These elements are configured using the EJB 3.2 XSD in the same way it is done for Java EE interceptors.

  3. Review the following descriptor file for examples of the above elements.

    Container Interceptor jboss-ejb3.xml File Example

    <jboss xmlns="http://www.jboss.com/xml/ns/javaee"
           xmlns:jee="http://java.sun.com/xml/ns/javaee"
           xmlns:ci ="urn:container-interceptors:1.0">
    
        <jee:assembly-descriptor>
            <ci:container-interceptors>
                <!-- Default interceptor -->
                <jee:interceptor-binding>
                    <ejb-name>*</ejb-name>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class>
                </jee:interceptor-binding>
                <!-- Class level container-interceptor -->
                <jee:interceptor-binding>
                    <ejb-name>AnotherFlowTrackingBean</ejb-name>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class>
                </jee:interceptor-binding>
                <!-- Method specific container-interceptor -->
                <jee:interceptor-binding>
                    <ejb-name>AnotherFlowTrackingBean</ejb-name>
                    <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class>
                    <method>
                        <method-name>echoWithMethodSpecificContainerInterceptor</method-name>
                    </method>
                </jee:interceptor-binding>
                <!-- container interceptors in a specific order -->
                <jee:interceptor-binding>
                    <ejb-name>AnotherFlowTrackingBean</ejb-name>
                    <interceptor-order>
                        <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class>
                        <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class>
                        <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class>
                    </interceptor-order>
                    <method>
                        <method-name>echoInSpecificOrderOfContainerInterceptors</method-name>
                    </method>
                </jee:interceptor-binding>
            </ci:container-interceptors>
        </jee:assembly-descriptor>
    </jboss>

The allow-ejb-name-regex attribute allows you to use regular expressions in interceptor bindings and maps the interceptors to all the beans that match the specified regular expression. Use the following management CLI command to enable the allow-ejb-name-regex attribute of the ejb3 subsystem to true:

/subsystem=ejb3:write-attribute(name=allow-ejb-name-regex,value=true)

The schema for the urn:container-interceptors:1.0 namespace is available at http://www.jboss.org/schema/jbossas/jboss-ejb-container-interceptors_1_0.xsd.

7.4. Change the Security Context Identity

By default, when you make a remote call to an EJB that is deployed to the application server, the connection to the server is authenticated and any subsequent requests that use the connection are executed using the original authenticated identity. This is true for both client-to-server and server-to-server calls. If you need to use different identities from the same client, normally you must open multiple connections to the server so that each one is authenticated as a different identity. Rather than open multiple client connections, you can give permission to the authenticated user to switch identities and execute a request on the existing connection as a different user.

Interceptors created and configured on the server-side are referred to as container interceptors. Interceptors created and configured on the client-side are referred to as client interceptors. To change the identity of a secured connection, you must create and configure the following three components.

Create and Configure the Client Interceptor

  1. Create the client interceptor.

    The client interceptor must implement the org.jboss.ejb.client.EJBClientInterceptor interface. The interceptor must pass the requested identity through the context data map, which can be obtained by using a call to EJBClientInvocationContext.getContextData(). The following is an example of a client interceptor that switches identities.

    Client Interceptor Code Example

    public class ClientSecurityInterceptor implements EJBClientInterceptor {
    
        public void handleInvocation(EJBClientInvocationContext context) throws Exception {
            Principal currentPrincipal = SecurityActions.securityContextGetPrincipal();
    
            if (currentPrincipal != null) {
                Map<String, Object> contextData = context.getContextData();
                contextData.put(ServerSecurityInterceptor.DELEGATED_USER_KEY, currentPrincipal.getName());
            }
            context.sendRequest();
        }
    
        public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception {
            return context.getResult();
        }
    }

  2. Configure the client interceptor.

    An application can insert a client interceptor into the EJBClientContext interceptor chain programmatically or by using the service loader mechanism. For instructions to configure a client interceptor, see Use a Client Interceptor in an Application.

Create and Configure the Container Interceptor

Container interceptor classes are simple Plain Old Java Objects (POJOs). They use the @javax.annotation.AroundInvoke annotation to mark the method that should be invoked during the invocation on the bean. For more information about container interceptors, see About Container Interceptors.

  1. Create the container interceptor.

    This interceptor receives the InvocationContext containing the identity and makes the request to switch to that new identity. The following is an abridged version of the actual code example:

    Container Interceptor Code Example

    public class ServerSecurityInterceptor {
    
        private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class);
    
        static final String DELEGATED_USER_KEY = ServerSecurityInterceptor.class.getName() + ".DelegationUser";
    
        @AroundInvoke
        public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
            Principal desiredUser = null;
            UserPrincipal connectionUser = null;
    
            Map<String, Object> contextData = invocationContext.getContextData();
            if (contextData.containsKey(DELEGATED_USER_KEY)) {
                desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY));
    
                Collection<Principal> connectionPrincipals = SecurityActions.getConnectionPrincipals();
    
                if (connectionPrincipals != null) {
                    for (Principal current : connectionPrincipals) {
                        if (current instanceof UserPrincipal) {
                            connectionUser = (UserPrincipal) current;
                            break;
                        }
                    }
    
                } else {
                    throw new IllegalStateException("Delegation user requested but no user on connection found.");
                }
            }
    
    
            ContextStateCache stateCache = null;
            try {
                if (desiredUser != null && connectionUser != null
                    && (desiredUser.getName().equals(connectionUser.getName()) == false)) {
                    // The final part of this check is to verify that the change does actually indicate a change in user.
                    try {
                        // We have been requested to use an authentication token
                        // so now we attempt the switch.
                        stateCache = SecurityActions.pushIdentity(desiredUser, new OuterUserCredential(connectionUser));
                    } catch (Exception e) {
                        logger.error("Failed to switch security context for user", e);
                        // Don't propagate the exception stacktrace back to the client for security reasons
                        throw new EJBAccessException("Unable to attempt switching of user.");
                    }
                }
    
                return invocationContext.proceed();
            } finally {
                // switch back to original context
                if (stateCache != null) {
                    SecurityActions.popIdentity(stateCache);;
                }
            }
        }

  2. Configure the container interceptor.

    For information on how to configure container interceptors, see Configure a Container Interceptor.

Create the JAAS LoginModule

The JAAS LoginModule component is responsible for verifying that the user is allowed to execute requests as the requested identity. The following abridged code example shows the methods that perform the login and validation:

LoginModule Code Example

    @SuppressWarnings("unchecked")
    @Override
    public boolean login() throws LoginException {
        if (super.login() == true) {
            log.debug("super.login()==true");
            return true;
        }

        // Time to see if this is a delegation request.
        NameCallback ncb = new NameCallback("Username:");
        ObjectCallback ocb = new ObjectCallback("Password:");

        try {
            callbackHandler.handle(new Callback[] { ncb, ocb });
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            // If the CallbackHandler can not handle the required callbacks then no chance.
            return false;
        }

        String name = ncb.getName();
        Object credential = ocb.getCredential();

        if (credential instanceof OuterUserCredential) {
            // This credential type will only be seen for a delegation request, if not seen then the request is not for us.

            if (delegationAcceptable(name, (OuterUserCredential) credential)) {
                identity = new SimplePrincipal(name);
                if (getUseFirstPass()) {
                    String userName = identity.getName();
                    if (log.isDebugEnabled())
                        log.debug("Storing username '" + userName + "' and empty password");
                    // Add the username and an empty password to the shared state map
                    sharedState.put("javax.security.auth.login.name", identity);
                    sharedState.put("javax.security.auth.login.password", "");
                }
                loginOk = true;
                return true;
            }
        }
        return false; // Attempted login but not successful.
    }

    // Make a trust user to decide if the user switch is acceptable.
    protected boolean delegationAcceptable(String requestedUser, OuterUserCredential connectionUser) {
    if (delegationMappings == null) {
        return false;
    }

    String[] allowedMappings = loadPropertyValue(connectionUser.getName(), connectionUser.getRealm());
    if (allowedMappings.length == 1 && "*".equals(allowedMappings[0])) {
        // A wild card mapping was found.
        return true;
    }
    for (String current : allowedMappings) {
        if (requestedUser.equals(current)) {
            return true;
        }
    }
    return false;
}

7.5. Use a Client Interceptor in an Application

An application can insert a client interceptor into the EJBClientContext interceptor chain either programmatically or by using the service loader mechanism.

Note

An EJBClientInterceptor can request specific data from the server side invocation context by calling org.jboss.ejb.client.EJBClientInvocationContext#addReturnedContextDataKey(String key). If the requested data is present under the provided key in the context data map, it is sent to the client.

Insert the Interceptor Programmatically

You must create an EJBClientContext with the interceptor registered.

EJBClientContext ctxWithInterceptors = EJBClientContext.getCurrent().withAddedInterceptors(clientInterceptor);

Once you have created the EJBClientContext, there are two ways to insert the interceptor:

  • You can run the following code with EJBClientContext applied using a Callable operation. EJB calls performed within the Callable operation will apply the client-side interceptors:

    ctxWithInterceptors.runCallable(() -> {
        // perform the calls which should use the interceptor
    })
  • Alternatively you can mark the newly created EJBClientContext as the new default:

    EJBClientContext.getContextManager().setThreadDefault(ctxWithInterceptors);

Insert the Interceptor Using the Service Loader Mechanism

Create a META-INF/services/org.jboss.ejb.client.EJBClientInterceptor file and place or package it in the class path of the client application. The rules for the file are dictated by the Java ServiceLoader Mechanism.

  • This file is expected to contain a separate line for each fully qualified class name of the EJB client interceptor implementation.
  • The EJB client interceptor classes must be available in the class path.

EJB client interceptors that are added using the service loader mechanism are added in the order they are found in the class path and are added to the end of the client interceptor chain.

Insert the Interceptor Using the ClientInterceptors Annotation

The @org.jboss.ejb.client.annotation.ClientInterceptors annotation allows you to place the EJB interceptor in the client-side of the remote call:

import org.jboss.ejb.client.annotation.ClientInterceptors;
@ClientInterceptors({HelloClientInterceptor.class})

public interface HelloBeanRemote {
   public String hello();
}