7.6.5. Pass Additional Security For EJB Authentication
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 pass additional information on the existing client connection for EJB authentication.
Procedure 7.13. Pass Security Information for EJB Authentication
Create the client side interceptor
This interceptor must implement theorg.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 toEJBClientInvocationContext.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 { 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(); } }
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”.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”.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 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);; } } }
Configure the container interceptor
For information on how to configure server side container interceptors, refer to: Section 7.6.3, “Configure a Container Interceptor”.
Create the JAAS LoginModule
This custom module performs the authentication using the existing authenticated connection information plus any additional security token. The following is a shortened example of code that uses the additional security token and performs the authentication. The complete code example can be viewed in theejb-security-interceptors
quickstart that ships with JBoss EAP 6.3 or later.public class DelegationLoginModule extends AbstractServerLoginModule { private static final String DELEGATION_PROPERTIES = "delegationProperties"; private static final String DEFAULT_DELEGATION_PROPERTIES = "delegation-mapping.properties"; private Properties delegationMappings; private Principal identity; @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { addValidOptions(new String[] { DELEGATION_PROPERTIES }); super.initialize(subject, callbackHandler, sharedState, options); String propertiesName; if (options.containsKey(DELEGATION_PROPERTIES)) { propertiesName = (String) options.get(DELEGATION_PROPERTIES); } else { propertiesName = DEFAULT_DELEGATION_PROPERTIES; } try { delegationMappings = loadProperties(propertiesName); } catch (IOException e) { throw new IllegalArgumentException(String.format("Unable to load properties '%s'", propertiesName), e); } } @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. }
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, theSaslPlusLoginModule
must be chained before the LoginModule that loads the roles with thepassword-stacking
option set.Configure the LoginModule Order using the Management CLI
The following is an example of Management CLI commands that chain the customSaslPlusLoginModule
before theRealmDirect
LoginModule that sets thepassword-stacking
option./subsystem=security/security-domain=quickstart-domain:add(cache-type=default)
/subsystem=security/security-domain=quickstart-domain/authentication=classic:add
/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})
/subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=RealmDirect:add(code=RealmDirect,flag=required,module-options={password-stacking=useFirstPass})
Configure the LoginModule Order Manually
The following is an example of XML that configures the LoginModule order in thesecurity
subsystem of the server configuration file. The customSaslPlusLoginModule
must precede theRealmDirect
LoginModule so that it can verify the remote user before the user roles are loaded and thepassword-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>
Create the Remote Client
In the following code example, assume theadditional-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 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()); } }