7.6.5. Pass Additional Security For EJB Authentication

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 using the credentials that authenticated the connection. Authentication at the connection level is dependent on the capabilities of the underlying SASL (Simple Authentication and Security Layer) mechanisms. Rather than write custom SASL mechanisms, you can open and authenticate a connection to the server, then later add additional security tokens prior to invoking an EJB. This topic describes how to to pass additional information on the existing client connection for EJB authentication.

The code examples below are for demonstration purposes only. They present only one possible approach and must be customized to suit the exact needs of the application. The password is exchanged using the SASL mechanism. If SASL DIGEST-MD5 Authentication is used, the password is still hashed with a challenge and not sent in the clear. The remaining tokens, however are sent in the clear. If those tokens contain any sensitive information, you may want to enable encryption for the connection.

Procedure 7.12. Pass Security Information for EJB Authentication

To supply an additional security token for an authenticated 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 additional security token 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 that creates an additional security token:
    public class ClientSecurityInterceptor implements EJBClientInterceptor {
    
        public void handleInvocation(EJBClientInvocationContext context) throws Exception {
            Object credential = SecurityActions.securityContextGetCredential();
    
            if (credential != null && credential instanceof PasswordPlusCredential) {
                PasswordPlusCredential ppCredential = (PasswordPlusCredential) credential;
                Map<String, Object> contextData = context.getContextData();
                contextData.put(ServerSecurityInterceptor.SECURITY_TOKEN_KEY, 
                                  ppCredential.getAuthToken());
            }
            context.sendRequest();
        }
    
        public Object handleInvocationResult(EJBClientInvocationContext context) 
                throws Exception {
            return context.getResult();
        }
    }
    
    
    For information on how to plug the client interceptor into an application, refer to Section 7.6.6, “Use a Client Side Interceptor in an Application”.
  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 is 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 retrieves the security authentication token from the context and passes it to the JAAS (Java Authentication and Authorization Service) domain for verification. The following is an example of container interceptor code:
      public class ServerSecurityInterceptor {
      
          private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class);
          static final String SECURITY_TOKEN_KEY = ServerSecurityInterceptor.class.getName() + ".SecurityToken";
      
          @AroundInvoke
          public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
              Principal userPrincipal = null;
              RealmUser connectionUser = null;
              String authToken = null;
      
              Map<String, Object> contextData = invocationContext.getContextData();
              if (contextData.containsKey(SECURITY_TOKEN_KEY)) {
                  authToken = (String) contextData.get(SECURITY_TOKEN_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;
                              }
                          }
                      }
                      userPrincipal = new SimplePrincipal(connectionUser.getName());
      
                  } else {
                      throw new IllegalStateException("Token authentication requested but no user on connection found.");
                  }
              }
      
              SecurityContext cachedSecurityContext = null;
              boolean contextSet = false;
              try {
                  if (userPrincipal != null && connectionUser != null && authToken != null) {
                      try {
                          // We have been requested to use an authentication token
                          // so now we attempt the switch.
                          cachedSecurityContext = SecurityActions.securityContextSetPrincipalCredential(userPrincipal,
                                  new OuterUserPlusCredential(connectionUser, authToken));
                          // 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 custom module performs the authentication using the existing authenticated connection information plus any additional security token. The following is an example of code that uses the additional security token and performs the authentication:
    public class SaslPlusLoginModule extends AbstractServerLoginModule {
    
        private static final String ADDITIONAL_SECRET_PROPERTIES = "additionalSecretProperties";
        private static final String DEFAULT_AS_PROPERTIES = "additional-secret.properties";
        private Properties additionalSecrets;
        private Principal identity;
    
        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
            addValidOptions(new String[] { ADDITIONAL_SECRET_PROPERTIES });
            super.initialize(subject, callbackHandler, sharedState, options);
            
            // Load the properties that contain the additional security tokens
            String propertiesName;
            if (options.containsKey(ADDITIONAL_SECRET_PROPERTIES)) {
                propertiesName = (String) options.get(ADDITIONAL_SECRET_PROPERTIES);
            } else {
                propertiesName = DEFAULT_AS_PROPERTIES;
            }
            try {
                additionalSecrets = SecurityActions.loadProperties(propertiesName);
            } catch (IOException e) {
                throw new IllegalArgumentException(String.format("Unable to load properties '%s'", propertiesName), e);
            }
        }
    
        @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 OuterUserPlusCredential) {
                OuterUserPlusCredential oupc = (OuterUserPlusCredential) credential;
                if (verify(name, oupc.getName(), oupc.getAuthToken())) {
                    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", oupc);
                    }
                    loginOk = true;
                    return true;
                }
            }
    
            return false; // Attempted login but not successful.
        }
    
        private boolean verify(final String authName, final String connectionUser, final String authToken) {
            // For the purpose of this quick start we are not supporting switching users, this login module is validation an
            // additional security token for a user that has already passed the sasl process.
            return authName.equals(connectionUser) && authToken.equals(additionalSecrets.getProperty(authName));
        }
    
        @Override
        protected Principal getIdentity() {
            return identity;
        }
    
        @Override
        protected Group[] getRoleSets() throws LoginException {
            Group roles = new SimpleGroup("Roles");
            Group callerPrincipal = new SimpleGroup("CallerPrincipal");
            Group[] groups = { roles, callerPrincipal };
            callerPrincipal.addMember(getIdentity());
            return groups;
        }
    }
    
    
  4. Add the Custom LoginModule to the Chain

    You must add the new custom LoginModule to the correct location the chain so that it is invoked in the correct order. In this example, the SaslPlusLoginModule must be chained before the LoginModule that loads the roles with the password-stacking option set.
    • Configure the LoginModule Order using the Management CLI

      The following is an example of Management CLI commands that chain the custom SaslPlusLoginModule before the RealmDirect LoginModule that sets the password-stacking option.
      [standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain:add(cache-type=default)
      [standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic:add
      [standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=DelegationLoginModule:add(code=org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule,flag=optional,module-options={password-stacking=useFirstPass})
      [standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=RealmDirect:add(code=RealmDirect,flag=required,module-options={password-stacking=useFirstPass})
      For more information about the Management CLI, refer to the chapter entitled Management Interfaces in the Administration and Configuration Guide for JBoss EAP 6 located on the Customer Portal at https://access.redhat.com/site/documentation/JBoss_Enterprise_Application_Platform/
    • Configure the LoginModule Order Manually

      The following is an example of XML that configures the LoginModule order in the security subsystem of the server configuration file. The custom SaslPlusLoginModule must precede the RealmDirect LoginModule so that it can verify the remote user before the user roles are loaded and the password-stacking option is set.
      <security-domain name="quickstart-domain" cache-type="default">
          <authentication>
              <login-module code="org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
              <login-module code="RealmDirect" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
          </authentication>
      </security-domain>
      
      
  5. Create the Remote Client

    In the following code example, assume the additional-secret.properties file accessed by the JAAS LoginModule above contains the following property:
    quickstartUser=7f5cc521-5061-4a5b-b814-bdc37f021acc
    
    
    The following code demonstrates how create the security token and set it before the the EJB call. The secret token is hard-coded for demonstration purposes only. This client simply prints the results to the console.
    import static org.jboss.as.quickstarts.ejb_security_plus.EJBUtil.lookupSecuredEJB;
    
    public class RemoteClient {
    
        /**
         * @param args
         */
        public static void main(String[] args) throws Exception {
            SimplePrincipal principal = new SimplePrincipal("quickstartUser");
            Object credential = new PasswordPlusCredential("quickstartPwd1!".toCharArray(), "7f5cc521-5061-4a5b-b814-bdc37f021acc");
    
            SecurityActions.securityContextSetPrincipalCredential(principal, credential);
            SecuredEJBRemote secured = lookupSecuredEJB();
    
            System.out.println(secured.getPrincipalInformation());
        }
    }