Extra Login Fields with Spring Security

Published in Java on 22-Nov-2011

The Spring framework is an excellent tool for accelerating and simplifying Java enterprise application development.  For many commonplace tasks, simply adding a few lines to an XML configuration file is all you need to introduce fairly complex functionality - eliminating the need for much of the boilerplate code that the application developer would otherwise have to implement.  

If you wanted to develop a website where users had to login - supplying a valid user name and password - before being able to access certain content, very little code is necessary.  The configuration necessary for this is well documented in the Spring 3.0 Documentation.

However, when it is necessary to develop a feature with Spring that deviates from the typical use-cases, it can be a little more complicated to configure correctly.

Use Case

Recently, I was developing a web portal for a client.  Part of the login process required that the user specify their username and password in addition to a company code.  Usernames would not necessarily be unique in the system, but the combination of username and company code would be unique.  The standard Spring Security configuration easily handles logins that require a username and a password, but how can this basic configuration be modified to handle what amounts to a multi-part username?

Implementation

Fortunately, Spring Security is easily extensible to allow customized behavior.  The key to implementing this kind of login solution is creating customized implementations of four fundamental elements:  The UserDetailsService interface, the AuthenticationProvider interface, the UserDetails interface, and the UsernamePasswordAuthenticationFilter class.

The UserDetails Object

To start with, we will extend the org.springframework.security.core.userdetails.User class, which implements the UserDetails interface. This is the class that will represent our user and contain information about their credentials and account. Most of the standard functionality and fields of the User class are still perfectly suitable for us, so we will simply add the behaviour we need to our subclass. The key difference, is that we require that this class track a company code.

public class MultiFieldLoginUserDetails extends User {
    private static final long serialVersionUID = 1L;
    private final String companyId;

    public MultiFieldLoginUserDetails(
            String username, String companyId, String password,
                Collection<GrantedAuthority> grantedAuthorities) {
        super(username, password, true, true, true, true, grantedAuthorities);
        this.companyId = companyId;
    }

    public String getCompanyId() {
        return this.companyId;
    }
}

Our extension of the User class takes the username, company code, password and collection of GrantedAuthority objects representing the user's roles and passes those values, with the exception of company code, along to the super class constructor. The company code value is stored in an instance variable and an accessor method String getCompanyId() is added. Instances of this class could be Data Transfer Objects created by some back-end service or even EJB 3.0 entity beans, representing rows in a database.

The UserDetailsService Object

Next, we will create a class called UserManagerService that implements the UserDetailsService interface from the org.springframework.security.core.userdetails package. This interface requires that a single method, UserDetails loadUserByUsername(String username), be implemented. The purpose of this class is to fetch a UserDetails object from whatever location we are storing user account information and return it to the rest of the Spring Security system for processing. Our implementation will look something like this:

public class UserManagerService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException, DataAccessException {
        final String[] split = StringUtils.splitPreserveAllTokens(username, "|");

        if (split.length != 2) {
            throw new IllegalArgumentException(
                "Username and company id not supplied correctly.  Value: " + username);
            )
        }

        final UserDetails result = getUser(split[0], split[1]);
        if (result == null) {
            throw new UsernameNotFoundException(
                "Could not locate user with username: " + username);
        }
        return result;
    }

    private UserDetails getUser(String username, String companyCode) {
        //Some code to lookup the user from a datastore based on the
        //username and companyCode.
    }
}

The object returned by the loadUserByUsername method will be an instance of the MultiFieldLoginUserDetails class we defined above. This method assumes that the username that will be passed in as an argument is a String containing two values delimited by a pipe character: The actual username and the company code.

This method uses the StringUtils.splitPreserveAllTokens method of the Apache Commons library to split the string into an array of elements. It will then pass the elements in the created array to the getUser method which will contain logic to actually lookup the User object.

The getUser method could execute a JDBC query, passing the username and companyCode as parameters and generate a MultiFieldLoginUserDetails object from the returned ResultSet object. Alternatively, it could lookup the User object from an in-memory list of users, an LDAP server, or it could simply delegate off to another service. The UserManagerService could even be a EJB 3.0 session bean and the getUser method could execute a JPA query to lookup the User based on the supplied information.

The AuthenticationProvider Object

This class is where we start getting into the real core of the solution. Authentication Providers are used for testing the credentials passed in to the Spring Security system against the system-known values and determining if the user should be authenticated. We will extend the abstract class AbstractUserDetailsAuthenticationProvider in the org.springframework.security.authentication.dao package and implement it's two abstract methods: additionalAuthenticationChecks and retrieveUser. Our Implementation follows:

public class MultiFieldAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    @Autowired
    protected UserDetailsService userDetailsService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

        if (!((String)authentication.getCredentials())
                    .equals(userDetails.getPassword())) {
            throw new BadCredentialsException("Password could not be validated.");
        }
    }

    @Override
    protected UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            throw new UsernameNotFoundException(
                    MessageFormat.format("User {0} could not be loaded", username));
        }
        return userDetails;
    }
}

Firstly, note that we are injecting a reference to a UserDetailsService via the @Autowired annotation. Later, when we create the Spring configuration to tie all our components together, we will ensure that the service which gets injected here is the UserManagerService we created above.

The retrieveUser method is responsible for looking up the UserDetails object associated with the username passed to the method as an argument and returning it. The username here is assumed to be the same pipe delimited username|companyCode string that we are expecting to handle in the UserManagerService described above. Here, this method just delegates to the UserDetailsService that we have injected.

The additionalAuthenticationChecks method is where we will actually test the validity of the password specified. We use the getCredentials method of the UsernamePasswordAuthenticationToken argument, which will return an Object (in this case of type String) which is the text entered by the user in the password field. We can then compare that value against the information provided in the retrieved UserDetails object. The authentication check is successful unless a BadCredentialsException is thrown.

Note: Obviously in this code, we are just comparing the text entered by the user to the plain text password stored in the UserDetails object. It should go without saying that this is not a best practice. Ideally, the password stored in the UserDetails object would be salted with a value unique to each user and hashed with a strong one-way algorithm like MD5 or SHA. In such a scenario, this method is an ideal place to take the plain text entered by the user, apply the same salting and hashing that the password in the user account has undergone and then compare that value in the UserDetails object. In the interests of brevity, we will not illustrate this here.

The AuthenticationFilter Object

The last object we must define is a filter to be inserted into the Spring Security filter chain which will handle the submission of our extra form field (the Company Code) and pass it along to the other components in the secrity system for use in authentication.

As we have already noted, what we are trying to accomplish here is very similar to a standard username/password login, with the complication that we need the username portion of the login to be composed of the information in two fields. Because of this similarity, we are able to inherit much of the default login logic by extending the UsernamePasswordAuthenticationFilter from the org.springframework.security.web.authentication package. Our extension of this class will look like this:

public class MultiFieldAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    protected String obtainUsername(HttpServletRequest request) {
        String superUsername = super.obtainUsername(request);
        String companyId = request.getParameter("j_companyid");
        if (superUsername == null) {
            superUsername = "";
        }
        if (companyId == null) {
            companyId = "";
        }
        return superUsername + "|" + companyId;
    }
}

Our enhancement to this class is very simple. We override the obtainUsername method which is intended to extract the value submitted by the user in the username field of the login form from the HttpServletRequest object and return it. Our implementation functions slightly differently from this default behaviour.

We first call the obtainUsername method of the super class to fetch the username that was submitted as part of the form. We then extract the parameter j_companyid from the HttpServletRequest and concatenate the two values together, delimited by a pipe character. This value is then returned and as a result, is fed into all the other methods that will conduct authentication as part of the Spring Security filter chain.

Configuration

Now that we have created all of the classes that we require, we must tell Spring Security to hook them all up together so that they provide us with the login behavior we require. To that end, we will be editing the applicationContext.xml file in the WEB-INF directory of our web application. This assumes that the basic setup steps of adding the DelegatingProxyFilter to your web.xml file, as described in the Spring 3.0 documentation, have already been followed.

If you were using the standard username/password login functionality of Spring Security, chances are you would have an <security:http> section in your applicationContext.xml file that resembled this:

<security:http auto-config="false">
    <security:intercept-url pattern="/menu.html*" access="ROLE_USER" />
    <security:intercept-url pattern="/login.html*" access="ROLE_ANONYMOUS, ROLE_USER" />

    <security:session-management invalid-session-url="/login.html?timeout=true" />
    <security:logout invalidate-session="false" logout-success-url="/login.html" />

    <security:form-login
        always-use-default-target="true"
        authentication-failure-url="/login.html?login_error=true"
        login-page="/login.html"
        default-target-url="/menu.html" />
</security:http>

As you can see, here we specify several rules for authorization, indicating which ROLES are allowed to access which URLs. We also specify the URLs of the login page, the successful login page, and the successful logout page and by virtue of adding the <security:form-login> tag, we tell Spring Security that we want to use Username and Password authentication. Some changes are required to this basic configuration in order to implement our solution.

Before tackling the modifications necessary to the <security:http> tag, we must define the beans that we will be using. Note that in the interests of legibility, I have abbreviate the package names in the following snippet (See the attached full source code of the sample application for the unmodified version):

<bean id="multiFieldAuthenticationFilter"
        class="c.q.b.m.s.MultiFieldAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationFailureHandler" ref="failureHandler" />
    <property name="authenticationSuccessHandler" ref="successHandler" />
</bean>

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

<bean id="authenticationProvider"
        class="c.q.b.m.s.MultiFieldAuthenticationProvider" />

<bean id="userManagerService"
        class="c.q.b.m.s.UserManagerService" />

<bean id="loginUrlAuthenticationEntryPoint"
        class="o.s.s.w.a.LoginUrlAuthenticationEntryPoint">
    <property name="loginFormUrl" value="/login.html" />
</bean>

<bean id="successHandler"
        class="o.s.s.w.a.SimpleUrlAuthenticationSuccessHandler">
    <property name="defaultTargetUrl" value="/menu.html" />
</bean>

<bean id="failureHandler"
        class="o.s.s.w.a.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="/login.html?login_error=true" />
</bean>

Here, we are defining beans that represent the AuthenticationProvider and the UserManagerService we created above. Further, we are configuring an AuthenticationManager to use our AuthenticationProvider and injecting that AuthenticationManager into the bean definition representing the MultiFieldAuthentationFilter we have defined.

Additionally, we have defined handlers, including a LoginUrlAuthenticationEntryPoint that specify the URLs that will be used by the authentication system (where previously we specified these in the <security:login-form> attribute).

Once our beans are defined, we can create our <security:http> attribute as follows:

<security:http auto-config='false' entry-point-ref="loginUrlAuthenticationEntryPoint">
    <security:intercept-url pattern="/menu.html*" access="ROLE_USER" />
    <security:intercept-url pattern="/login.html*" access="ROLE_USER, ROLE_ANONYMOUS" />
    <security:session-management invalid-session-url="/login.html?timeout=true" />

    <security:custom-filter position="FORM_LOGIN_FILTER"
        ref="multiFieldAuthenticationFilter" />

    <security:logout invalidate-session="false" logout-success-url="/login.html" />
</security:http>

The three differences are highlighed above.

  1. We have added the entry-point-ref attribute to the <security:http> element which will allow the handler beans we defined above to be used.
  2. We have removed the <security:form-login> tag (hence the need for the entry-point-ref attribute: As we are no longer using the form-login tag, which configures the standard build-in Username and Password authentication behavior, we can no longer use the form-login attributes to define the various URLs used by the authentication system.
  3. We have added the <security:custom-filter> tag which allows us to specify our custom authentication behavior.

We specify that our new authentication filter, represented by the multiFieldAuthenticationFilter bean is to be used in the Spring Security filter chain at the FORM_LOGIN_FILTER position. This ensures that when our application attempts to authenticate the user, our custom logic is executed, and we are logged into the application using a combination of a username, a company code, and a password.

All that still remains is to ensure that the login JSP at reachable via /login.html contains a form field called j_companyid in addition to the standard j_username and j_password.

Conclusion

As you can see, it is quite easy to extend the default behaviors provided by the Spring Security framework to allow us to customize the authentication process to account for more complex login use-cases. Extending the functionality of a small number of classes and then configuring those extended classes to be used as part of the Spring bean configuration is all that is necessary.

I hope that some out there will find this article useful. When I was implementing it originally, I found that I had to do quite a bit of code and API reading, googling and research to figure out exactly how all the moving parts fit together. Hopefully I have been able to save some of you from having to do that legwork. I would appreciate any feedback, comments or corrections.

Complete Source Code

The source code for a complete, bare-bones sample application is available in zipped format.  It can be built using Maven 3.0 and was tested on Apache Tomcat 6 - although it should deploy successfully on any JEE Compatible container. 

multiFieldSpringLoginSource.zip

F68KBZU2QXVP

References 

Spring 3.0 Security Namespace Configuration Documenation

Spring 3.0 Security Filter Chain Documentation

Apache Commons StringUtils API


About the Author

dan.jpg

Daniel Morton is a Software Developer with Shopify Plus in Waterloo, Ontario and the co-owner of Switch Case Technologies, a software development and consulting company.  Daniel specializes in Enterprise Java Development and has worked and consulted in a variety of fields including WAN Optimization, Healthcare, Telematics, Media Publishing, and the Payment Card Industry.