Red Hat Training

A Red Hat training course is available for Red Hat JBoss Enterprise Application Platform

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 the * 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 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.

The abridged code examples that follow are taken from the ejb-security-interceptors quickstart that ships with JBoss EAP. This quickstart is a simple Maven project that provides a working example of how to switch identities on an existing connection.

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

Note

See the ejb-security-interceptors quickstart README.html file for complete instructions and more detailed information about the code examples.

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.

Insert the Interceptor Programmatically.

Call the org.jboss.ejb.client.EJBClientContext.registerInterceptor(int order, EJBClientInterceptor interceptor) method and pass the order and the interceptor instance. The order determines where this client interceptor is placed in the interceptor chain.

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. The ejb-security-interceptors quickstart that ships with Red Hat JBoss Enterprise Application Platform uses this approach.