7.6.4. Change the Security Context Identity

Summary

By default, when you make a remote call to an EJB deployed to the application server, the connection to the server is authenticated and any request received over this connection is executed as the identity that authenticated the connection. This is true for both client-to-server and server-to-server calls. If you need to use different identities from the same client, you normally need to 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 execute a request as a different user.

This topic describes how to to switch identities on the existing client connection. Refer to the ejb-security-interceptors quickstart for a complete working example. The following code examples are abridged versions of the code in the quickstart.

Procedure 7.11. Change the Identity of the Security Context

To change the identity of a secured connection, you must create the following 3 components.
  1. Create the client side interceptor

    This interceptor must implement the org.jboss.ejb.client.EJBClientInterceptor. The interceptor is expected to pass the requested identity through the context data map, which can be obtained via a call to EJBClientInvocationContext.getContextData(). The following is an example of client side interceptor code:
    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();
        }
    }
    
    
    User applications can then plug in the interceptor in the EJBClientContext in one of the following ways:
    • Programmatically

      With this approach, you call the org.jboss.ejb.client.EJBClientContext.registerInterceptor(int order, EJBClientInterceptor interceptor) API and pass the order and the interceptor instance. The order is used to determine where exactly in the client interceptor chain this interceptor is placed.
    • ServiceLoader Mechanism

      This approach requires the creation of a META-INF/services/org.jboss.ejb.client.EJBClientInterceptor file and placing or packaging it in the classpath of the client application. The rules for the file are dictated by the Java ServiceLoader Mechanism. This file is expected to contain in each separate line the fully qualified class name of the EJB client interceptor implementation. The EJB client interceptor classes must be available in the classpath. EJB client interceptors added using the ServiceLoader mechanism are added to the end of the client interceptor chain, in the order they are found in the classpath. The ejb-security-interceptors quickstart uses this approach.
  2. Create and configure the server side container interceptor

    Container interceptor classes are simple Plain Old Java Objects (POJOs). They use the @javax.annotation.AroundInvoke to mark the method that will be invoked during the invocation on the bean. For more information about container interceptors, refer to: Section 7.6.1, “About Container Interceptors”.
    1. Create the container interceptor

      This interceptor receives the InvocationContext with the identity and requests the switch. The following is an abridged version of the actual 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;
                  RealmUser connectionUser = null;
      
                  Map<String, Object> contextData = invocationContext.getContextData();
                  if (contextData.containsKey(DELEGATED_USER_KEY)) {
                      desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY));
                      Connection con = SecurityActions.remotingContextGetConnection();
                      if (con != null) {
                          UserInfo userInfo = con.getUserInfo();
                          if (userInfo instanceof SubjectUserInfo) {
                              SubjectUserInfo sinfo = (SubjectUserInfo) userInfo;
                              for (Principal current : sinfo.getPrincipals()) {
                                  if (current instanceof RealmUser) {
                                      connectionUser = (RealmUser) current;
                                      break;
                                  }
                              }
                          }
                      } else {
                          throw new IllegalStateException("Delegation user requested but no user on connection found.");
                      }
                  }
      
                  SecurityContext cachedSecurityContext = null;
                  boolean contextSet = false;
                  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 switch user and have successfully identified the user from the connection
                              // so now we attempt the switch.
                              cachedSecurityContext = SecurityActions.securityContextSetPrincipalInfo(desiredUser,
                                      new OuterUserCredential(connectionUser));
                              // keep track that we switched the security context
                              contextSet = true;
                              SecurityActions.remotingContextClear();
                          } 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 security context
                      if (contextSet) {
                          SecurityActions.securityContextSet(cachedSecurityContext);
                      }
                  }
              }
          }
      
    2. Configure the container interceptor

      For information on how to configure server side container interceptors, refer to: Section 7.6.3, “Configure a Container Interceptor”.
  3. Create the JAAS LoginModule

    This component is responsible for verifying that user is allowed to execute requests as the requested identity. The following code examples show the methods that peform the login and validation:
    @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;
            }
            return false; // If the CallbackHandler can not handle the required callbacks then no chance.
        }
        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.
    }
    
    
    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[1])) {
            // A wild card mapping was found.
            return true;
        }
        for (String current : allowedMappings) {
            if (requestedUser.equals(current)) {
                return true;
            }
        }
        return false;
    }
    
    
See the quickstart README file for complete instructions and more detailed information about the code.