5/1/09   Propagating Identity from Spring Security to the EJB Layer

A problem I recently encountered myself appears to be topic frequently brought up on support forums: if you choose to use Spring Security rather than Container Managed Security (for reasons of portability or flexibility or whatever), how do you access the current user identity from within EJBs?

EJBs have long had both a declarative and programmatic security model, the latter of which provides access to the current user. The current user identify is frequently required, for example for audit purposes when persisting data changes.

JBoss and ClientLoginModule

The solution is to find some way to programmatically log the user in to the container's security context: pre-authenticated by Spring, of course, we don't want to duplicate authentication within the same application! The JAAS API provides a ProgrammaticLogin construct which allows this but in JBoss there is a much simpler solution.

The default JBoss JAAS configuration includes a login module called ClientLoginModule. This module 'trusts' the information provided to it and is "Used by clients within the application server VM such as mbeans and servlets that access EJBs". Perfect! All we need to do is use the JAAS API to login using this module.

Once this is done, we can use the standard EJB sessionContext.getCallerPrincipal().getName() method to obtain the current user. Note that no role or group information is propagated to JAAS using this mechanism.

CallbackHandler callbackHandler = new CallbackHandler {
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        for (Callback callback : callbacks) {
            if (callback instanceof NameCallback) {
                final String principal = 
                      SecurityContextHolder.getContext().getAuthentication().getName(); // Current user principal
                ((NameCallback) callback).setName(principal);
            } 
        }
    }
LoginContext loginContext = new LoginContext("client-login", callbackHandler);
loginContext.login();
.. do the EJB calls here ..
loginContext.logout();

As you can see from the above example, we log in and out of the security context before and after issuing our EJB calls. If we wrap the whole request using a filter then we can be automatically logged in to JAAS for the whole duration of the request, without any extra application code.

Below is presented the JBossSecurityFilter. It can be installed in the filter chain using the configuration XML below:

 	<beans:bean id="jbossSecurityFilter" class="com.tapina.example.JBossSecurityFilter">
		<custom-filter after="SERVLET_API_SUPPORT_FILTER"/>
		<beans:property name="clientLoginDomain" value="client-login"/>
		<beans:property name="callbackHandler">
			<beans:bean class="com.tapina.example.SecurityContextHolderAwareCallbackHandler"/>
		</beans:property>
	</beans:bean>

JBossSecurityFilter

import java.io.IOException;

import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginContext;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.util.ReflectionUtils;

public class JBossSecurityFilter extends SpringSecurityFilter {
    private String clientLoginDomain = "client-login";
    private CallbackHandler callbackHandler = new SecurityContextHolderAwareCallbackHandler();
    private LoginContext loginContext = null;
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
    
    @Override
    protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || trustResolver.isAnonymous(authentication)) {
            chain.doFilter(request, response); // No login
            return;
        }
        try {
            if (loginContext == null) {
                    loginContext = new LoginContext(clientLoginDomain, callbackHandler);
            }
            loginContext.login();
            chain.doFilter(request, response);
            loginContext.logout();
        } catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
        }
    }

    public int getOrder() {
        return FilterChainOrder.SERVLET_API_SUPPORT_FILTER + 1;
    }

    public void setClientLoginDomain(String clientLoginDomain) {
        this.clientLoginDomain = clientLoginDomain;
    }

    public void setCallbackHandler(CallbackHandler callbackHandler) {
        this.callbackHandler = callbackHandler;
    }
    
}

SecurityContextHolderAwareCallbackHandler

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.springframework.security.context.SecurityContextHolder;

public class SecurityContextHolderAwareCallbackHandler implements CallbackHandler {
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        for (Callback callback : callbacks) {
            if (callback instanceof NameCallback) {
                final String principal = SecurityContextHolder.getContext().getAuthentication().getName(); 
                ((NameCallback) callback).setName(principal);
            } 
        }
        
    }
}