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 switch identities on the existing client connection. The code examples are abridged versions of the code in the quickstart. Refer to the ejb-security-interceptors quickstart for a complete working example.
Procedure 7.12. Change the Identity of the Security Context
To change the identity of a secured connection, you must create the following 3 components.
Create the client side interceptor
The client side 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 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();
}
}
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();
}
}
Copy to ClipboardCopied!Toggle word wrapToggle overflow
User applications can insert the interceptor into the interceptor chain 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) method and pass the order and the interceptor instance. The order determines where this client interceptor is placed in the interceptor chain.
ServiceLoader Mechanism
With this approach, you create a META-INF/services/org.jboss.ejb.client.EJBClientInterceptor file and place or package 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 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 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.
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”.
Create the container interceptor
This interceptor receives the InvocationContext with the identity and requests the switch to that new identity. 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;
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);;
}
}
}
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);;
}
}
}
Copy to ClipboardCopied!Toggle word wrapToggle overflow
This component is responsible for verifying that user is allowed to execute requests as the requested identity. The following abridged 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;
}
@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;
}
Copy to ClipboardCopied!Toggle word wrapToggle overflow
See the ejb-security-interceptors quickstart README.html file for complete instructions and more detailed information about the code.