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.

Friday, July 1, 2011

Spring + Configuration

A real world web application (like any other application) needs to be configured somehow. Sometimes this configuration can be known in the compile-time, but some configuration will be provided by an administrator later.

In order to demonstrate two examples of configuration, I am going to pick the application you saw this post (XML-RPC sample) and modify some parts of it to:
1- Populate the properties files using Maven in the compile-time. Here I will use a plug-in to read SVN meta-data and pass the revision number to the properties files.
2- Read the application's dynamic configuration using JNDI. A JDBC datasource is a very good example.
And, I am not going to write any new code to do so.

There is a full working code for this blog post in here that can be checked out using this command:
svn checkout http://tinywebgears-samples.googlecode.com/svn/trunk/contextsample contextsample

1- Populate the properties files:

First of all we are going to have a properties file to define all we need in (contextsample-services.properties):
# These properties are defined in the master pom.xml file
product.name: ${mavenproperties.product.name}
product.version: ${mavenproperties.product.version}
# These properties are populated by maven-svn-revision-number-plugin
product.svn.revision: ${svnpluginproperties.revision}


Then, we need to ask Maven to filter the resource fiels, so that those ${param} placeholders are replaced by actual values.
     <resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>


We have defined some parameters in the pom.xml file that will be used to filter properties files:
<properties>
<!-- Properties exposed by the application -->
<mavenproperties.product.name>${name}</mavenproperties.product.name>
<mavenproperties.product.version>${version}</mavenproperties.product.version>
</properties>


We also need to use this plug-in to read the SVN meta-data. This plug-in will provide svnpluginproperties.* properties accessed in our configuration.

         <plugin>
<groupid>com.google.code.maven-svn-revision-number-plugin</groupid>
<artifactid>maven-svn-revision-number-plugin</artifactid>
<version>${revisionNumberPluginVersion}</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<entries>
<entry>
<prefix>svnpluginproperties</prefix>
</entry>
</entries>
</configuration>
</plugin>


Now, if you compile your code you will have a contextsample-services.properties file (in the target path) that has the right values for parameters. You might want to read this file in your code, or in your Spring configuration file using a tag like this:

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


This way you can access the values of those parameters as ${product.name}, ${product.version}, and ${product.svn.revision} in the bean definitions.

Please note that the properties file we ended up with will be bundled in the application and is not a good place to put your dynamic configuration, because no one will be able to modify them easily after you made the project artifacts.

2- Read dynamic configuration

There are different ways to achieve this goal, but in this post you will see a very easy way of doing so using JNDI. One advantage of it is that you don't need to declare the path of configuration file anywhere in the application. If your application has so many configuration parameters it is possible to declare the path to a properties file (including those parameters) this way and read the whole file in the way mentioned above. Your application is tied to name of JNDI paremeters which is not a big deal.

You don't need a full J2EE application server to do this since most of the Servlet containers have this capability. The instructions provided here will work on Tomcat 5+, and it is very likely to be different for other application servers.

First of all you need to create a META-INF/context.xml file containing these lines:

<!--?xml version="1.0" encoding="UTF-8"?-->

<!-- Context configuration file for the Data Library XML-RPC services -->

<context docbase="${catalina.home}/webapps/contextsample-services" privileged="true" antiresourcelocking="false" antijarlocking="false">

<!-- Define parameters -->
<environment name="config/environment" value="prod 1" type="java.lang.String" override="true">

</environment></context>


Then, all you need to do is reference the parameters provided by JNDI in your Spring context configuration file:

 <bean id="sampleService" class="com.tinywebgears.samples.service.SampleServiceImpl">
<constructor-arg index="0">
<jee:jndi-lookup name="config/environment">
</jee:jndi-lookup></constructor-arg>
...
</bean>


The trick is that Tomcat will get this context.xml file and copy it somewhere in the configuration directory so that it can be edited easily by the administrator. If the file is already there it won't be overwritten, to avoid accidentally overriding hard work of administrators. Please read this section from Tomcat docs for more details. Here is an excerpt from that document:

Only if a context file does not exist for the application in the <code>$CATALINA_HOME/conf/[enginename]/[hostname]/</code>; in an individual file at <code>/META-INF/context.xml</code> inside the application files. If the web application is packaged as a WAR then <code>/META-INF/context.xml</code> will be copied to <code>$CATALINA_HOME/conf/[enginename]/[hostname]/</code> and renamed to match the application's context path. Once this file exists, it will not be replaced if a new WAR with a newer <code>/META-INF/context.xml</code> is placed in the host's appBase.

Please note that providing an <environment> tag in the context.xml is equivalent of providing an <env-entry> tag in the web.xml file. If you provide both you will probably override your application context configuration file.

UPDATE: If you deploy your application by dumping WAR file in Tomcat's webapps folder, this does not apply to you. Tomcat will undeploy your application first (removing the context configuration as well, dropping all the changes you might have done to it). See this post for a solution.