« Back to Single Sign-on

Upstream authenticating reverse-proxies, SPNEGO, REMOTE_USER, getRemoteUser()

In some single sign on (SSO) solutions, the portal may delegate user authentication entirely to an upstream reverse-proxy.  Direct access to the portal is prevented - all access is through the reverse-proxy, which identifies the user and passes their identity to the portal.

An example of such a setup is Microsoft IIS with Windows Authentication enabled.  In this case we will use Apache Tomcat as the portal appserver, but similar can be achieved with other appservers.

Portal users on an intranet with Internet Explorer can authenticate to IIS using SPNEGO, see also http://msdn.microsoft.com/en-us/library/ms995329.aspx .  If combined with an ISAPI plugin such as the Tomcat Connector for IIS (AJP13), with the AJP connector configured with tomcatAuthentication="false", IIS will set the CGI variable REMOTE_USER to the principal, the Active Directory authenticated user.  The result is that Tomcat can read the current user name via HttpServletRequest.getRemoteUser().  For Windows Active Directory, this would typically be in the form DOMAIN\username.

AutoLogin extensions and the AutoLoginFilter #

At this point it seems a straightforward exercise of using an existing or custom Liferay AutoLogin extension, to process the remote username data and automatically log the user in.  The AutoLoginFilter when activated processes a list of configured AutoLogin extensions.  The contract of String[] AutoLogin.login(request, response) is to return an array of Liferay (internal) UserID (long), password (String), passwordEncrypted (boolean), converting request/session data into a Liferay user.

The way that Liferay handles getRemoteUser() might be surprising to a developer attempting to implement an AutoLogin module:

  • Liferay expects getRemoteUser() to return the numeric (long) internal Liferay UserID (user primary key) for the authenticated user.  An upstream/remote reverse-proxy might have a number of ways of identifying a user principal, but the internal Liferay UserID seems an unlikely one.
  • if getRemoteUser() returns not-null, the AutoLoginFilter considers the user to be logged-on to the portal, and no AutoLogin modules are executed.  This feature is documented in in LPS-8637  , “AutoLoginFilter.java cannot call the AutoLogin class defined in auto.login.hooks property”, which discusses the getRemoteUser() == null test before evaluating AutoLogin modules.

Ideally we'd like to be able to:

  • Use all or part of getRemoteUser() as a screenName or emailAddress, and lookup the Liferay user from that.
  • If the Liferay user does not exist, use on-demand LDAP import features to see if the user can be imported from LDAP.

So what can be done to allow this given the logic surrounding getRemoteUser()?  Some developers have customized AutoLoginFilter to modify the logic around getRemoteUser().  The solution presented here prefers a custom front filter for reasons detailed below.

Custom filter, reuse CASAutoLogin #

This article has nothing to do with CAS and using it for authentication, other than the fact that we reuse part of the existing Liferay CAS auto-login logic and classes to reduce additional coding.  Normally CASAutoLogin it relies on a CASFilter to run before it to perform the actual CAS login handshake, but we can replace the CASFilter with a custom Filter which follows the CASAutoLogin conventions, and then reuse CASAutoLogin.  CASAutoLogin already has code for the Liferay user-lookup by screenName or emailAddress, as well as on-demand LDAP import, so by using it none of that code needs to be repeated.

CASAutoLogin works by retrieving the already authenticated user screenName from the session and matching this to the Liferay user, performing LDAP imports if enabled and needed.  By reusing the CAS session attribute name, we can reuse the CASAutoLogin.  So we need a custom filter to:

  • Process getRemoteUser() data.  This is because Windows Authentication will send the user as DOMAIN\username, whereas we want just the username part to match a screenName and LDAP data.
  • Copy the extracted username to the session attribute expected by CASAutoLogin.
  • Wrap the HttpServletRequest to nullify getRemoteUser().  This allows AutoLoginFilter to activate CASAutoLogin.

The filter:

package custom.filter;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.log4j.Logger;

public class RemoteUserFilter implements Filter {
	
	private static final Logger logger = Logger.getLogger(RemoteUserFilter.class);
	
	protected static final String INIT_PARAM_FORCE_REMOTE_USER = "forceRemoteUser";
	
	protected static final String INIT_PARAM_NULLIFY_REMOTE_USER = "nullifyRemoteUser";
	
	protected static final String INIT_PARAM_TARGET_SESSION_ATTRIBUTE_NAME = "sessionAttributeName";
	
	protected static final String INIT_PARAM_TARGET_REQUEST_ATTRIBUTE_NAME = "requestAttributeName";
	
	protected static final String INIT_PARAM_REGEX = "regex";
	
	private String forcedRemoteUser;
	
	private boolean nullifyRemoteUser;
	
	private String sessionAttributeName;
	
	private String requestAttributeName;
	
	private Pattern pattern;
	
	public void init(FilterConfig filterConfig) throws ServletException {
		logger.info("Filter init");
		
		forcedRemoteUser = filterConfig.getInitParameter(INIT_PARAM_FORCE_REMOTE_USER);
		logger.info("forcedRemoteUser: " + forcedRemoteUser);
		
		nullifyRemoteUser = Boolean.parseBoolean(filterConfig.getInitParameter(INIT_PARAM_NULLIFY_REMOTE_USER));
		logger.info("nullifyRemoteUser: " + nullifyRemoteUser);
		
		sessionAttributeName = filterConfig.getInitParameter(INIT_PARAM_TARGET_SESSION_ATTRIBUTE_NAME);
		logger.info("sessionAttributeName: " + sessionAttributeName);
		
		requestAttributeName = filterConfig.getInitParameter(INIT_PARAM_TARGET_REQUEST_ATTRIBUTE_NAME);
		logger.info("requestAttributeName: " + requestAttributeName);
		
		if (sessionAttributeName == null && requestAttributeName == null) {
			throw new ServletException("Missing init parameter - at least one of sessionAttributeName or requestAttributeName must be set.");
		}
		
		final String regex = filterConfig.getInitParameter(INIT_PARAM_REGEX);
		logger.info("regex: " + regex);
		if (regex != null && regex.length() > 0) {
			pattern = Pattern.compile(regex);
		}
	}

	public void doFilter(ServletRequest request, final ServletResponse response,
			final FilterChain filterChain) throws IOException, ServletException {
		
		if (request instanceof HttpServletRequest) {
			final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
			
			// forced user by init param, may be null
			String remoteUser = forcedRemoteUser;
						
			if (remoteUser == null) {
				remoteUser = httpServletRequest.getRemoteUser();
			} else {
				logger.warn("remoteUser is set statically in filter config: " + INIT_PARAM_FORCE_REMOTE_USER + "=" + remoteUser);
			}

			logger.debug("remoteUser: " + remoteUser);
			
			if (remoteUser != null && remoteUser.length() > 0) {
				if (pattern != null) {
					remoteUser = applyRegex(remoteUser);
				}
				
				// set a session attribute
				if (sessionAttributeName != null) {
					addSessionAttribute(httpServletRequest, sessionAttributeName, remoteUser);
				}
				
				// set a request attribute
				if (requestAttributeName != null) {
					addRequestAttribute(httpServletRequest, requestAttributeName, remoteUser);
				}
				
				if (nullifyRemoteUser) {
					logger.debug("Wrapping servlet request to force getRemoteUser() to return null");
					request = wrapRequest(httpServletRequest);
				}
			} 
		}
		
		filterChainDoFilter(filterChain, request, response);
	}

	protected String applyRegex(String remoteUser) {
		Matcher matcher = pattern.matcher(remoteUser);
		if (matcher.groupCount() == 1) {
			if (matcher.matches()) {
				remoteUser = matcher.group(1);
				logger.debug("Regex applied, new remoteUser=" + remoteUser);
			} else {
				logger.warn("No regex match for " + remoteUser);
			}
		} else {
			logger.warn("Matcher should only have one group, but has " + matcher.groupCount());
		}
		
		return remoteUser;
	}
	
	protected void addSessionAttribute(HttpServletRequest httpServletRequest, String sessionAttributeName, String remoteUser) {
		logger.debug("Setting session attribute::value " + sessionAttributeName + "::" + remoteUser);
		httpServletRequest.getSession().setAttribute(sessionAttributeName, remoteUser);
	}
	
	protected void addRequestAttribute(HttpServletRequest httpServletRequest, String sessionAttributeName, String remoteUser) {
		logger.debug("Setting request attribute::value " + requestAttributeName + "::" + remoteUser);
		httpServletRequest.setAttribute(requestAttributeName, remoteUser);
	}

	protected HttpServletRequest wrapRequest(final HttpServletRequest requestToWrap) {
		return new HttpServletRequestWrapper(requestToWrap) {
			@Override
			public String getRemoteUser() {
				logger.debug("Returning null for getRemoteUser()");
				return null;
			}
		};
	}
	
	protected void filterChainDoFilter(FilterChain filterChain, ServletRequest request, ServletResponse response) throws IOException, ServletException {
		filterChain.doFilter(request, response);
	}
	
	public void destroy() {
		// NOOP
	}

}

This needs to be installed as a filter in the portal's web.xml, with init params:

  • nullifyRemoteUser = true

sessionAttributeName = <value of com.liferay.portal.servlet.filters.sso.cas.CASFilter.LOGIN>

  • regex =

.*\\(.*)

0 Attachments
7959 Views
Average (0 Votes)
The average rating is 0.0 stars out of 5.
Comments