The Awnry Bear

Two-Factor Authentication and Spring Security 3

First off, what exactly does “two-factor authentication” mean? Two-factor authentication simply adds a second credential in addition to the username (and besides the password) on which to authenticate. For example, in addition to a username to couple with a password, you may require the user to enter their corporate domain, client name, or state.

Two-Factor Authentication

If you’re just getting started with Spring Security, you probably have realized how steep of a learning curve there is. Even if you understand the high-level architecture, making things fit together and getting that combination just right can be a frustrating experience. So in this article, I’m going to show you how to get two-factor authentication working with a Spring web application.

"Single-Factor" Authentication with Spring Security

Let’s look at the configuration for “single-factor” authentication, i.e. username and password. We’re going to go with a combination of using the namespace and using straight beans.

<beans:beans
	xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
			http://www.springframework.org/schema/beans
			http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

			http://www.springframework.org/schema/security
			http://www.springframework.org/schema/security/spring-security-3.0.xsd">

	<http use-expressions="true" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
		<intercept-url pattern="/secured" access="isAuthenticated()" />
		<intercept-url pattern="/**" access="permitAll" />
		<custom-filter position="FORM_LOGIN_FILTER" ref="usernamePasswordAuthenticationFilter" />
		<logout logout-url="/logout" />
	</http>

	<authentication-manager alias="authenticationManager">
		<authentication-provider ref="authenticationProvider" />
	</authentication-manager>

	<beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
		<beans:property name="passwordEncoder">
			<beans:bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" />
		</beans:property>
		<beans:property name="userDetailsService" ref="userService" />
	</beans:bean>

	<beans:bean id="userService" class="com.awnry.springexample.UserDetailsServiceImpl" />

	<beans:bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<beans:property name="loginFormUrl" value="/login" />
	</beans:bean>

	<beans:bean id="usernamePasswordAuthenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationFailureHandler" ref="failureHandler" />
		<beans:property name="authenticationSuccessHandler" ref="successHandler" />
		<beans:property name="filterProcessesUrl" value="/processLogin" />
		<beans:property name="postOnly" value="true" />
	</beans:bean>

	<beans:bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
		<beans:property name="defaultTargetUrl" value="/login" />
	</beans:bean>

	<beans:bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<beans:property name="defaultFailureUrl" value="/login?login_error=true" />
	</beans:bean>

</beans:beans>

There isn’t much to explain here, especially if you’re familar with using a namespace-beans hybrid approach to configuring Spring Security. The only class we have to provide here is a UserDetailsService implementation; here is a simple one for completeness sake:

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService
{
	@Autowired
	private UserDao userDao;


	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException
	{
		User User = userDao.findByUsername(username);

		System.out.println("Returning user: " + user);
		return user;
	}
}

All our UserDetailsService does is return the User with the given username. The details of the DAO and how the retrieval is done is not important; what is important is that we have a UserDetailsService implementation that can be used with Spring’s UsernamePasswordAuthenticationFilter.

Adding the Two-Factor Authentication Filter

So now that we have basic username/password authentication working, let’s make the necessary changes to handle a second input field, such as the user’s corporate domain.

The first thing we need to do is extend the UsernamePasswordAuthenticationFilter class so that it can handle a second input field.

/**
 * A custom {@link UsernamePasswordAuthenticationFilter} that adds two-factor authentication.
 * A user enters a username and an "extra" input along with a password, and the username and extra
 * input values are returned through the <code>obtainUsername()</code> method as a colon-delimited
 * string. The delimiter string (a colon by default) can be customized with the
 * <code>setDelimiter()</code> method.
 *
 * @author Jonathon Freeman
 */
public class TwoFactorAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
	private String extraParameter = "extra";
	private String delimiter = ":";


	/**
	 * Given an {@link HttpServletRequest}, this method extracts the username and the extra input
	 * values and returns a combined username string of those values separated by the delimiter
	 * string.
	 *
	 * @param request The {@link HttpServletRequest} containing the HTTP request variables from
	 *   which the username client domain values can be extracted
	 */
	@Override
	protected String obtainUsername(HttpServletRequest request)
	{
		String username = request.getParameter(getUsernameParameter());
		String extraInput = request.getParameter(getExtraParameter());

		String combinedUsername = username + getDelimiter() + extraInput;

		System.out.println("Combined username = " + combinedUsername);
		return combinedUsername;
	}

	/**
	 * @return The parameter name which will be used to obtain the extra input from the login request
	 */
	public String getExtraParameter()
	{
		return this.extraParameter;
	}

	/**
	 * @param extraParameter The parameter name which will be used to obtain the extra input from the login request
	 */
	public void setExtraParameter(String extraParameter)
	{
		this.extraParameter = extraParameter;
	}

	/**
	 * @return The delimiter string used to separate the username and extra input values in the
	 * string returned by <code>obtainUsername()</code>
	 */
	public String getDelimiter()
	{
		return this.delimiter;
	}

	/**
	 * @param delimiter The delimiter string used to separate the username and extra input values in the
	 * string returned by <code>obtainUsername()</code>
	 */
	public void setDelimiter(String delimiter)
	{
		this.delimiter = delimiter;
	}
}

This class may look nasty, but it’s really pretty simple. The real work is happening in the obtainUsername() method. What this method does is retrieve the username and “extra” input field from the HttpServletRequest object that’s passed in. It then concatenates these two values into one string, separating them by the delimiter string (a colon, by default). It then returns this combined string. The parameter from which the “extra” input field is read is extra by default, but just like the delimiter string this parameter name can be customized which we will illustrate later.

Combining the username and the extra field into one string that is returned by the obtainUsername() method is a quick and simple way of reusing components that work with the single-factor authentication components provided by Spring. There are other ways we could acheive two-factor authentication, but this is one of the easiest ways.

Updating UserDetailsService

Now that we have created an authentication filter that supports two-factor authentication, we need to update the loadUserByUsername() method in our UserDetailsService implementation. The change needed involves extracting the username and the extra input field from the username string that is returned by our TwoFactorAuthenticationFilter's obtainUsername() method, so that we can query our list of users using both values. Here is our updated loadUserByUsername() method:

@Override
public UserDetails loadUserByUsername(String input) throws UsernameNotFoundException, DataAccessException
{
	String[] split = input.split(":");
	if(split.length < 2)
	{
		System.out.println("User did not enter both username and corporate domain.");
		throw new UsernameNotFoundException("Must specify both username and corporate domain");
	}

	String username = split[0];
	String domain = split[1];

	System.out.println("Username = " + username);
	System.out.println("Corporate domain = " + domain);

	User user = userDao.findByUsernameAndDomain(username, domain);
	if(user == null)
	{
		System.out.println("User could not be found, must be an invalid username/domain combo.");
		throw new UsernameNotFoundException("Invalid username or corporate domain");
	}

	System.out.println("Returning user: " + user);
	return user;
}

It’s pretty straightforward. First, we split the given username into its two components: the username and the extra field. In this example, the extra field is the user’s corporate domain. Once we have the username and the domain, we can use our DAO to find the matching user.

That’s all the Java code necessary to implement two-factor authentication! Pretty simple, huh? The great thing about modifying these two classes is that the rest of the Spring Security components can work with them without being none the wiser, and we don’t have to make lots of modifications to the configuration. Speaking of the configuration…

Tying It All Together

The last piece of the puzzle is to update our configuration to use our new TwoFactorAuthenticationFilter:

<beans:beans
	xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
			http://www.springframework.org/schema/beans
			http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

			http://www.springframework.org/schema/security
			http://www.springframework.org/schema/security/spring-security-3.0.xsd">

	<http use-expressions="true" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
		<intercept-url pattern="/secured" access="isAuthenticated()" />
		<intercept-url pattern="/**" access="permitAll" />
		<custom-filter position="FORM_LOGIN_FILTER" ref="twoFactorAuthenticationFilter" />
		<logout logout-url="/logout" />
	</http>

	<authentication-manager alias="authenticationManager">
		<authentication-provider ref="authenticationProvider" />
	</authentication-manager>

	<beans:bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
		<beans:property name="passwordEncoder">
			<beans:bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder" />
		</beans:property>
		<beans:property name="userDetailsService" ref="userService" />
	</beans:bean>

	<beans:bean id="userService" class="com.awnry.springexample.UserDetailsServiceImpl" />

	<beans:bean id="loginUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<beans:property name="loginFormUrl" value="/login" />
	</beans:bean>

	<beans:bean id="twoFactorAuthenticationFilter" class="com.awnry.springexample.TwoFactorAuthenticationFilter">
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationFailureHandler" ref="failureHandler" />
		<beans:property name="authenticationSuccessHandler" ref="successHandler" />
		<beans:property name="filterProcessesUrl" value="/processLogin" />
		<beans:property name="postOnly" value="true" />
		<beans:property name="extraParameter" value="domain" />
	</beans:bean>

	<beans:bean id="successHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
		<beans:property name="defaultTargetUrl" value="/login" />
	</beans:bean>

	<beans:bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<beans:property name="defaultFailureUrl" value="/login?login_error=true" />
	</beans:bean>

</beans:beans>

There you have it. In the twoFactorAuthenticationFilter bean definition, we set the extraParameter property to “domain” which is the name of the input field to use in our login form. If we wanted, we could also customize the parameter names for the username and password fields as well as the delimiter string to something other than a colon. Remember if you decide to change the delimiter string, you’ll need to update your UserDetailsService implementation since it has the colon hardwired in.

I hope this article removes some of the fear out of using Spring Security beyond what the standard namespace configuration provides. Once you’re comfortable with the architecture of the Spring Security framework, making modifications such as adding two-factor authentication to accomplish what your unique situation calls for becomes a breeze thanks to the flexibility and clean design of the Spring Security API.