Sunday, July 17, 2011

Custom Authentication using Spring Security

Today I will describe how to use Spring Security (3.0.5) to implement a form-based authentication using your own authentication service (well, one of the ways I did so!). I will show you a sample web application with a dummy authentication and remember-me (SSO) service in this post.
The source code for this sample can be found here, or checked out on your own machine using this command:
svn checkout http://tinywebgears-samples.googlecode.com/svn/trunk/customauth customauth

To run the application you could either compile it with maven or import it in your favourite IDE, then deploy the WAR file in a Servlet container. The web application will be available on http://host:port/customauth/ url (replace host and port with your own).The dummy authentication service has two user accounts you might use to log in: user/user which is a normal user, and admin/admin which is an administrator. The way that dummy SSO works is a bit funny though, I couldn't think of a better way of doing so without storing user sessions: If you have an authenticated session and restart your application (by either redeploying it or restarting the container) you have 30 seconds to re-use your SSO session, otherwise you have to log in again.

Our sample application has a few XML files (web.xml, customauth-servlet.xml, applicationContext.xml, and applicationContext-security.xml) to specify how the web application and Spring beans are configured. Lets have a look at a big picture to see what we are going to achieve using these configuration files (click for the big image):


I had to implement four classes (highlighted in the diagram), one enum for the role, and one controller class. Please browse the source code for more details, they are too big to be shown here. Now we will take a peek inside configuration files.

web.xml configures the web application and servlets.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">

<display-name>Spring security web application (series)</display-name>

<!-- to specifically stop trouble with multiple apps on tomcat -->
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>customauth_root</param-value>
</context-param>

<!-- Location of the XML file that defines the root application context
applied by ContextLoaderListener. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
   WEB-INF/applicationContext.xml
   WEB-INF/applicationContext-security.xml
     </param-value>
</context-param>

<!-- Loads the root application context of this web app at startup. The
application context is then available via WebApplicationContextUtils.getWebApplicationContext(servletContext). -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Provides core MVC application controller. See customauth-servlet.xml. -->
<servlet>
<servlet-name>customauth</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>customauth</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>customauth</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>


customauth-servlet.xml configures Spring MVC controllers.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
 http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-3.0.xsd
 ">

<!-- no 'id' required, HandlerMapping beans are automatically detected by
the DispatcherServlet -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
 <prop key="/*.htm">urlController</prop>
 <prop key="/*.do">actionController</prop>
</props>
</property>
</bean>

<bean id="actionController"
class="com.tinywebgears.samples.customauth.controller.ActionController" />

<bean id="urlController"
class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />

<!-- as we have our jsp in an internal place forcing all requests through
spring, use viewResolver to save us making reference to internal structure
everywhere e.g. /WEB-INF/jsp/ -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="attributes">
<props>
 <prop key="message">Static Message</prop>
</props>
</property>
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

</beans>


applicationContext.xml configures the <sec:http> tag.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security"
xmlns:p="http://www.springframework.org/schema/p"
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
   ">

<sec:http auto-config="false" entry-point-ref="authenticationEntryPoint">
<sec:custom-filter position="REMEMBER_ME_FILTER"
ref="rememberMeFilter" />
<sec:custom-filter position="FORM_LOGIN_FILTER"
ref="umsAuthenticationProcessingFilter" />
<sec:custom-filter position="LOGOUT_FILTER" ref="umsLogoutFilter" />

<sec:intercept-url pattern="/login.jsp" filters="none" />
<sec:intercept-url pattern="/denied.jsp" filters="none" />
<sec:intercept-url pattern="/admin.htm" access="ROLE_ADMIN" />
<sec:intercept-url pattern="/**" access="ROLE_USER" />

<sec:access-denied-handler ref="accessDeniedHandler" />
</sec:http>

<bean id="accessDeniedHandler"
class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/denied.jsp" />
</bean>
</beans>


applicationContext-security does the main job by wiring all those things you saw in the diagram together.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
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
   ">

<global-method-security secured-annotations="disabled" />

<beans:bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<beans:property name="location">
<beans:value>classpath:ums.properties</beans:value>
</beans:property>
</beans:bean>

<!-- Filters -->

<beans:bean id="rememberMeFilter"
class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<!-- custom-filter position="REMEMBER_ME_FILTER"/ -->
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="rememberMeServices" ref="umsRememberMeServices" />
</beans:bean>

<beans:bean id="umsLogoutFilter"
class="com.tinywebgears.samples.customauth.service.UmsLogoutFilter">
<!-- custom-filter position="LOGOUT_FILTER" / -->
<beans:constructor-arg value="/login.jsp?loggedout=true" />
<beans:constructor-arg>
<beans:list>
 <beans:ref bean="umsRememberMeServices" />
 <beans:bean id="logoutHandler"
  class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler">
 </beans:bean>
</beans:list>
</beans:constructor-arg>
<beans:property name="cookieName" value="${Ums.SSO.Cookie.Name}" />
<beans:property name="filterProcessesUrl" value="/j_spring_security_logout" />
</beans:bean>

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

<beans:bean id="umsAuthenticationProcessingFilter"
class="com.tinywebgears.samples.customauth.service.UmsAuthenticationProcessingFilter">
<beans:property name="cookieName" value="${Ums.SSO.Cookie.Name}" />
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="umsUserDetailsService" ref="umsUserDetailsService" />
<beans:property name="authenticationSuccessHandler">
<beans:bean id="authenticationSuccessHandler"
 class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"
 p:alwaysUseDefaultTargetUrl="false" p:defaultTargetUrl="/home.htm" />
</beans:property>
<beans:property name="authenticationFailureHandler">
<beans:bean id="authenticationFailureHandler"
 class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"
 p:defaultFailureUrl="/login.jsp?authfailed=true" />
</beans:property>
</beans:bean>


<!-- Authentication Manager -->

<!-- This will override the settings of authentication manager bean. -->
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="umsUserDetailsService">
<password-encoder hash="sha" base64="true" />
</authentication-provider>
<authentication-provider ref="rememberMeAuthenticationProvider" />
</authentication-manager>


<!-- Beans and Providers -->

<beans:bean id="umsUserDetailsService"
class="com.tinywebgears.samples.customauth.service.UmsUserDetailsService">
</beans:bean>

<beans:bean id="rememberMeAuthenticationProvider"
class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
<!-- This ensures that remember-me is added as an authentication provider -->
<beans:property name="key" value="${Ums.SSO.Cookie.Key}" />
</beans:bean>

<beans:bean id="umsRememberMeServices"
class="com.tinywebgears.samples.customauth.service.UmsRememberMeServices">
<beans:property name="userDetailsService" ref="umsUserDetailsService" />
<beans:property name="cookieName" value="${Ums.SSO.Cookie.Name}" />
<beans:property name="key" value="${Ums.SSO.Cookie.Key}" />
</beans:bean>

</beans:beans>


You can get this code as a starting point and modify those classes to use your desired authentication service.

Note: Spring security 3.0.4 which I was using first has some bugs related to sec:http tag which prevented forwarding to error pages properly. Please note that error pages are forwarded to, not redirected to (see here if you don't know the difference). It is strongly recommended to update your libraries if you are having issues.