Simple web application with Spring Security: Part 6

We have received feeback from our customer on the stories that we have implemented so far. In relation to the message received when authentication failed, they found it annoying that when they entered there username, password that they didn’t know which one was wrong. So a change in the requirements for user story 5 was made to read like so:

User Story 5: Users that fail authentication should be presented with the login page and a reason for the failure.

Note: The username used to login should remain present on the form.

Note: When the failure is because the username does not exist then this should be made clear in the failure message.

So for the first time we are presented with adding a behavior to the system that spring security namespace configuration does not support out of the box.

Adding custom behavior

Spring security by default does not throw UsernameNotFoundException when the username entered is not found druing authentication but instead throws BadCredentialsException. see the authenticate method of org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvider
.

The DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider and this is the provider that we are currently using by default in our examples. However there is no way using spring security’s namespace configuration to alter the hideUserNotFoundExceptions property which defaults to true.

To enable UsernameNotFoundException to be thrown we must customize the AUTHENTICATION_PROCESSING_FILTER and replace our custom one with the one that is created out of the box by spring security.

We already have acceptance tests around the failure message return for incorrect username and password.

Step 1: Lets update the username acceptance test to expect something else.

@Test
    public void shouldRemainOnLoginPageWithInformativeMessageWhenAuthenticationFailsDueToIncorrectUsername() {

        // try to get to home.htm
        driver.get("http://localhost:8080/springsecuritywebapp/login.jsp");

        final WebElement usernameField = driver.findElement(By
                .name("j_username"));
        usernameField.sendKeys("username_does_not_exist");

        final WebElement passwordField = driver.findElement(By
                .name("j_password"));
        passwordField.sendKeys("password");

        passwordField.submit();

        // state verification
        assertThat(driver.getTitle(),
                is("Login: Spring Security Web Application"));

        final WebElement informationMessageSection = driver.findElement(By
                .id("infomessage"));
        assertThat(
                informationMessageSection.getText(),
                containsString("Login failed due to: Could not find user: username_does_not_exist."));
    }

Step 2: Lets update our applicationContext-security.xml to add a custom authentication processing filter.

<beans:bean id="customAuthenticationProcessingFilter"
		class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
		<custom-filter position="AUTHENTICATION_PROCESSING_FILTER" />
		<beans:property name="defaultTargetUrl" value="/home.htm" />
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationFailureUrl" value="/login.jsp?authfailed=true" />
		<beans:property name="allowSessionCreation" value="true" />
	</beans:bean>

Things to note:

  1. The details that we had in our form-login configuration element are been passed in here to our custom authenticationProcessingFilter

The custom AuthenticationProcessingFilter requires an authenticationManager so lets define our custom one:

<beans:bean id="authenticationManager"
		class="org.springframework.security.providers.ProviderManager">
		<beans:property name="providers">
			<beans:list>
				<beans:ref local="daoAuthenticationProvider" />
			</beans:list>
		</beans:property>
		<beans:property name="sessionController"
			ref="defaultConcurrentSessionController" />
</beans:bean>

If we want to keep our concurrent session functionality we must remember to provide the authentication manager with one. Here is how we set it up:

<beans:bean id="sessionRegistry"
		class="org.springframework.security.concurrent.SessionRegistryImpl" />

<beans:bean id="defaultConcurrentSessionController"
		class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl">
        <beans:property name="sessionRegistry" ref="sessionRegistry" />
	<beans:property name="exceptionIfMaximumExceeded" value="true" />
</beans:bean>

The custom authenticationManager requires a daoAuthenticationProvider which is the class we want customise to set hideUserNotFoundExceptions to false:

<beans:bean id="daoAuthenticationProvider"
		class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
	<beans:property name="userDetailsService" ref="userDetailsService" />
	<beans:property name="hideUserNotFoundExceptions" value="false" />
</beans:bean>

Things to note:

  1. We pass in the userDetailService bean that contains our user credentials
  2. We override the hideUserNotFoundException to be false, this will cause spring security to throw UserNameNotFoundException instead of BadCredentialsException.

For this to work we need to modify our http configuration configure it to use a custom AuthenticationEntryPoint:

<beans:bean id="myAuthenticationEntryPoint"
		class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
	<beans:property name="loginFormUrl" value="/login.jsp" />
</beans:bean>

<http entry-point-ref="myAuthenticationEntryPoint" auto-config="false">
	<intercept-url pattern="/login.jsp" filters="none" />
	<intercept-url pattern="/**" access="ROLE_USER" />
	<!--
		no longer needed as using custom authtication approach <form-login
		login-page="/login.jsp" default-target-url="/home.htm"
		always-use-default-target="false"
		authentication-failure-url="/login.jsp?authfailed=true" />
	-->
	<logout invalidate-session="true" logout-url="/logout.htm"
			logout-success-url="/login.jsp?loggedout=true" />

		<!--
			make sure you have
			org.springframework.security.ui.session.HttpSessionEventPublisher
			registered in the web.xml file.
		-->
               <!-- no longer configured from here
		<concurrent-session-control max-sessions="1"
			exception-if-maximum-exceeded="true" />
               -->
</http>

Things to note:

  1. We created our own AuthenticationEntryPoint which points to our login form
  2. We reference the authenticationEntryPoint on the http element
  3. We must disable auto-config and we must remove the form-login configuration element. The information that was here is not been passed to the authentication processing filter by our custom bean.

The entire applicationContext-security.xml file now looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<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-2.5.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">

	<beans:bean id="sessionRegistry"
		class="org.springframework.security.concurrent.SessionRegistryImpl" />

	<beans:bean id="defaultConcurrentSessionController"
		class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl">
		<beans:property name="sessionRegistry" ref="sessionRegistry" />
		<beans:property name="exceptionIfMaximumExceeded"
			value="true" />
	</beans:bean>

	<beans:bean id="daoAuthenticationProvider"
		class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
		<beans:property name="userDetailsService" ref="userDetailsService" />
		<beans:property name="hideUserNotFoundExceptions"
			value="false" />
	</beans:bean>

	<beans:bean id="authenticationManager"
		class="org.springframework.security.providers.ProviderManager">
		<beans:property name="providers">
			<beans:list>
				<beans:ref local="daoAuthenticationProvider" />
			</beans:list>
		</beans:property>
		<beans:property name="sessionController"
			ref="defaultConcurrentSessionController" />
	</beans:bean>

	<beans:bean id="customAuthenticationProcessingFilter"
		class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
		<custom-filter position="AUTHENTICATION_PROCESSING_FILTER" />
		<beans:property name="defaultTargetUrl" value="/home.htm" />
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationFailureUrl" value="/login.jsp?authfailed=true" />
		<beans:property name="allowSessionCreation" value="true" />
	</beans:bean>

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

	<beans:bean id="myAuthenticationEntryPoint"
		class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
		<beans:property name="loginFormUrl" value="/login.jsp" />
	</beans:bean>

	<http entry-point-ref="myAuthenticationEntryPoint" auto-config="false">
		<intercept-url pattern="/login.jsp" filters="none" />
		<intercept-url pattern="/admin.htm" access="ROLE_ADMIN" />
		<intercept-url pattern="/**" access="ROLE_USER" />
		<!--
			no longer needed as using custom authtication approach <form-login
			login-page="/login.jsp" default-target-url="/home.htm"
			always-use-default-target="false"
			authentication-failure-url="/login.jsp?authfailed=true" />
		-->
		<logout invalidate-session="true" logout-url="/logout.htm"
			logout-success-url="/login.jsp?loggedout=true" />

		<!--
			make sure you have
			org.springframework.security.ui.session.HttpSessionEventPublisher
			registered in the web.xml file.
		-->
		<!-- no longer used - config set in bean -->
		<!--
			<concurrent-session-control max-sessions="1"
			exception-if-maximum-exceeded="true" />
		-->

	</http>

	<authentication-provider>
		<user-service id="userDetailsService">
			<user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
			<user name="username" password="password" authorities="ROLE_USER" />
			<user name="test" password="test" authorities="ROLE_USER" />
		</user-service>
	</authentication-provider>
</beans:beans>

Build, deploy and run all acceptance tests to verify they pass with this change in configuration.

Getting the code

The code for this part is tagged and available for viewing online at: http://code.google.com/p/spring-security-series/source/browse/#svn/tags/SpringSecuritySeriesWAR-Part6

SVN Url: https://spring-security-series.googlecode.com/svn/tags/SpringSecuritySeriesWAR-Part6

5 Responses to “Simple web application with Spring Security: Part 6”

  1. afernandes Says:

    Congratulations, great sample!!

    I would only suggest a little modification at applicationContext-security.xml to make the two controls of concurrent session be possible, by two controls I mean:
    1- Prevent a second login or
    2- Second login cause the first one to be invalidated.

    Follow the changes below:
    uncomment
    <!–
    <concurrent-session-control max-sessions=”1″
    exception-if-maximum-exceeded=”true” />
    –>
    and changed to
    <concurrent-session-control session-registry-alias=”sessionRegistry”/>

    Then comment
    <beans:bean id=”sessionRegistry” class=”org.springframework.security.concurrent.SessionRegistryImpl”/>
    In this way you could use
    <beans:property name=”exceptionIfMaximumExceeded” value=”true”/>
    or
    <beans:property name=”exceptionIfMaximumExceeded” value=”false”/>
    and achieve the wished control.

    …and thank you by the sample.

  2. shmulik Says:

    For security reasons, it is wrong to tell which value was incorrect, the user name or the password.
    You do not want to give a hacker any hint that might make his hacking life easier

  3. Simon Baker Says:

    The SessionRegistryImpl works only on the current node. Do you have a sample of how to enforce the “one session per user” requirement across multiple application server nodes? Perhaps using a DB to store the session registry information?

  4. Aishwarya Says:

    want to add some additional security if a user fails more than thrice… help me out..

  5. eros Says:

    @Aishwarya

    >want to add some additional security if a user fails more than thrice… help me out..
    With this, you need to store the number of tries. e.g. in the database

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: