Simple web application with Spring Security: Part 7

More feedback was received from our customer and the customer want a slight tweak to user story 2. They want a user that was in an admin role to be automatically brought to the admin page and users in user role’s would be brought to the home page after a successful login.

User story 2 described now as:

User Story 2: With a valid username/password a user should be able to log in and view the application home page.

Note: Users that have an admin role should be automatically brought to the admin page by default.

To do this, we will need to determine at what point spring security works out the url to send an authenticated user to and customize this to meet our needs.

Determining target url

After some debugging and looking at the spring security code, we see that within the AbstractProcessingFilter there is a method called determineTargetUrl that is responsible for determining the target url to send the user to after successful authentication.

The code for it is:

protected String determineTargetUrl(HttpServletRequest request) {
        // Don't attempt to obtain the url from the saved request if alwaysUsedefaultTargetUrl is set
    	String targetUrl = alwaysUseDefaultTargetUrl ? null :
    		targetUrlResolver.determineTargetUrl(getSavedRequest(request), request, SecurityContextHolder.getContext().getAuthentication());

        if (targetUrl == null) {
            targetUrl = getDefaultTargetUrl();
        }

        return targetUrl;
}

From the code, we can see that the responsibility for working out the target url is delegated out to a targetUrlResolver. It is passed the saved request, the request and the users authentication object. The cleanest way for us to implement our functionality will be to implement our own TargetUrlResolver and to configure the AUTHENTICATION_PROCESSING_FILTER to use our resolver instead of the default one.

Implementing our TargetUrlResolver

The code:

package com.heraclitus.web;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.ui.TargetUrlResolver;
import org.springframework.security.ui.TargetUrlResolverImpl;
import org.springframework.security.ui.savedrequest.SavedRequest;

import com.heraclitus.domain.Role;

/**
 * I am responsible for determining what url should be the targetUrl for the
 * user based on their {@link GrantedAuthority}.
 *
 * If I cant match any of the {@link GrantedAuthority}'s with our application
 * {@link Role}'s I default back to the {@link TargetUrlResolverImpl}
 * implementation provided by spring security.
 */
public class RoleBasedTargetUrlResolverImpl implements TargetUrlResolver {

    private static final String ADMIN_ROLE_TARGET_URL = "/admin.htm";

    private final TargetUrlResolver defaultSpringSecurityUrlResolver;

    public RoleBasedTargetUrlResolverImpl(
            final TargetUrlResolver fallbackTargetUrlResolver) {
        defaultSpringSecurityUrlResolver = fallbackTargetUrlResolver;
    }

    /**
     * I determine the target url based on the {@link Authentication} and
     * whether their {@link GrantedAuthority} match any of our applications
     * {@link Role} 's.
     */
    public String determineTargetUrl(final SavedRequest savedRequest,
            final HttpServletRequest currentRequest, final Authentication auth) {

        if (containsAdminAuthority(auth)) {
            return ADMIN_ROLE_TARGET_URL;
        }

        return defaultSpringSecurityUrlResolver.determineTargetUrl(
                savedRequest, currentRequest, auth);
    }

    private boolean containsAdminAuthority(final Authentication auth) {
        for (final GrantedAuthority grantedAuthority : auth.getAuthorities()) {
            if (grantedAuthority.getAuthority().equals(
                    Role.ADMIN_ROLE.roleName())) {

                return true;
            }
        }
        return false;
    }
}

Things to note:

Code for Role’s

package com.heraclitus.domain;

/**
 * I describe the different application roles that exist
 */
public enum Role {

    ADMIN_ROLE("ROLE_ADMIN", 1), USER_ROLE("ROLE_USER", 0);

    private final int order;
    private final String roleName;

    private Role(final String roleName, final int order) {
        this.roleName = roleName;
        this.order = order;
    }

    public int order() {
        return order;
    }

    public String roleName() {
        return roleName;
    }

}

Unit tests:

package com.heraclitus.web;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.ui.TargetUrlResolver;

import com.heraclitus.domain.Role;
import com.heraclitus.spring.security.AuthenticationConfigurableStub;
import com.heraclitus.spring.security.GrantedAuthorityConfigurableStub;

/**
 * I test {@link RoleBasedTargetUrlResolverImpl}.
 */
@SuppressWarnings("synthetic-access")
@RunWith(JMock.class)
public class RoleBasedTargetUrlResolverImplTest {

    // collaborators/peers
    private TargetUrlResolver fallbackTargetUrlResolver;

    // mockery
    private final Mockery mockery = new JUnit4Mockery();

    // class under test
    private RoleBasedTargetUrlResolverImpl roleBasedTargetUrlResolver;

    @Before
    public void setupTestMethod() {
        fallbackTargetUrlResolver = mockery.mock(TargetUrlResolver.class);

        roleBasedTargetUrlResolver = new RoleBasedTargetUrlResolverImpl(
                fallbackTargetUrlResolver);
    }

    @Test
    public void shouldReturnAdminTargetUrlWhenAdminAuthorityExists() {

        // setup
        final GrantedAuthority adminAuthority = new GrantedAuthorityConfigurableStub(
                Role.ADMIN_ROLE);
        final GrantedAuthority[] authorities = new GrantedAuthority[] { adminAuthority };

        final Authentication auth = new AuthenticationConfigurableStub(
                authorities);

        // exercise test
        final String determinedTargetUrl = roleBasedTargetUrlResolver
                .determineTargetUrl(null, null, auth);

        // state verification
        assertThat(determinedTargetUrl, is("/admin.htm"));
    }

    @Test
    public void shouldDelegateToFallbackTargetResolverWhenAdminAuthorityDoesNotExist() {

        // setup
        final GrantedAuthority userAuthority = new GrantedAuthorityConfigurableStub(
                Role.USER_ROLE);
        final GrantedAuthority[] authorities = new GrantedAuthority[] { userAuthority };

        final Authentication auth = new AuthenticationConfigurableStub(
                authorities);

        // behavior verification
        mockery.checking(new Expectations() {
            {
                one(fallbackTargetUrlResolver).determineTargetUrl(null, null,
                        auth);
                will(returnValue("/anyTargetUrl"));
            }
        });

        // exercise test
        final String determinedTargetUrl = roleBasedTargetUrlResolver
                .determineTargetUrl(null, null, auth);

        // state verification
        assertThat(determinedTargetUrl, is("/anyTargetUrl"));
    }
}

Acceptance tests:

@Test
    public void shouldForwardUsersWithAdminRoleToTheAdminPageAfterLogin() {

        loginAsUser(driver, withAdminRole());

        assertThat(driver.getTitle(),
                is("Admin: Spring Security Web Application"));
    }

The applicationContext-security.xml file:

<?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="defaultTargetUrlResolver"
		class="org.springframework.security.ui.TargetUrlResolverImpl" />

	<beans:bean id="roleBasedTargetUrlResolver"
		class="com.heraclitus.web.RoleBasedTargetUrlResolverImpl">
		<beans:constructor-arg ref="defaultTargetUrlResolver" />
	</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:property name="targetUrlResolver" ref="roleBasedTargetUrlResolver" />
	</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>

As always, build, deploy run tests and verify all are passing.

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-Part7

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

10 Responses to “Simple web application with Spring Security: Part 7”

  1. heden Says:

    WOW,great job. amazing.

    some minor suggestions let you guys know.

    this line private static final String ADMIN_ROLE_TARGET_URL = “/admin.htm”;

    still seems hard code. if it can be injected as a property from spring config XML file will make it more flexible.

    another idea is for test case. same idea

    for example:

    // try to get to home.htm
    driver.get(“http://localhost:8080/springsecuritywebapp/home.htm”);

    // check we are on our custom login page
    assertThat(driver.getTitle(),
    is(“Login: Spring Security Web Application”));

    final WebElement usernameField = driver.findElement(By
    .name(“j_username”));
    usernameField.sendKeys(“username”);

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

    string

    http://localhost:8080/springsecuritywebapp

    “j_username”,”username”

    “j_password”,”password”

    can set by a property file or create a interface to hold these contants

    but the whole codes you guys made are really beautiful.

    thanks a lot.

    Merry X’mas and Happy New Year.

    heden

    • heraclitusonsoftware Says:

      Thanks for the comments heden. Agreed that the value for ADMIN_ROLE_TARGET_URL would be configured through some properties rather than hard coded as is. It was just done this way for convenience as I am trying to concentrate on talking about spring security features and working with spring security. I will add post sometime dealing with properties etc. As for the test code, we can certainly improve upon it but thats a post for a different topic..

      as always any input into things you would like to discuss in the series are welcome.

      Merry Christmas and Happy new year,
      heraclitus

  2. heden Says:

    Hi heraclitus:

    totally agree.focusing on the main architecure ,then refactor later.actually,I do same thing.

    and I am lucky to find your training here.

    thanks

    heden

  3. madhava Says:

    Hi

    i nwant to redirect usre to a different page on credentials expired exception

    auto config=false

  4. Stevi Deter Says:

    Thanks for this post. I have a very similar requirement, and this got me headed in the right direction. I appreciate the time you’ve saved me having to work this out myself!

  5. Christian Maslen Says:

    Thanks for the post and the series. I thought I’d convert the unit test to use Mockito. Here is the code in case you’re interested:

    package com.heraclitus.web;

    import static org.hamcrest.CoreMatchers.is;
    import static org.junit.Assert.assertThat;
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.times;

    import org.junit.Before;
    import org.junit.Test;
    import org.springframework.security.Authentication;
    import org.springframework.security.GrantedAuthority;
    import org.springframework.security.ui.TargetUrlResolver;

    import com.heraclitus.domain.Role;

    /**
    * I test {@link RoleBasedTargetUrlResolverImpl}.
    */
    public class RoleBasedTargetUrlResolverImplTest {

    // collaborators/peers
    private TargetUrlResolver fallbackTargetUrlResolver;

    // class under test
    private RoleBasedTargetUrlResolverImpl roleBasedTargetUrlResolver;

    @Before
    public void setupTestMethod() {
    fallbackTargetUrlResolver = mock(TargetUrlResolver.class);

    roleBasedTargetUrlResolver = new RoleBasedTargetUrlResolverImpl(
    fallbackTargetUrlResolver);
    }

    @Test
    public void shouldReturnAdminTargetUrlWhenAdminAuthorityExists() {

    // setup
    final GrantedAuthority adminAuthority = mock(GrantedAuthority.class);
    when(adminAuthority.getAuthority()).thenReturn(
    Role.ADMIN_ROLE.toString());

    final GrantedAuthority[] authorities = new GrantedAuthority[] { adminAuthority };

    final Authentication auth = mock(Authentication.class);
    when(auth.getAuthorities()).thenReturn(authorities);

    when(fallbackTargetUrlResolver.determineTargetUrl(null, null, auth))
    .thenReturn(“/admin.htm”);

    // exercise test
    final String determinedTargetUrl = roleBasedTargetUrlResolver
    .determineTargetUrl(null, null, auth);

    // state verification
    assertThat(determinedTargetUrl, is(“/admin.htm”));
    }

    @Test
    public void shouldDelegateToFallbackTargetResolverWhenAdminAuthorityDoesNotExist() {

    // setup
    final GrantedAuthority userAuthority = mock(GrantedAuthority.class);
    when(userAuthority.getAuthority())
    .thenReturn(Role.USER_ROLE.toString());
    final GrantedAuthority[] authorities = new GrantedAuthority[] { userAuthority };
    final Authentication auth = mock(Authentication.class);
    when(auth.getAuthorities()).thenReturn(authorities);

    when(fallbackTargetUrlResolver.determineTargetUrl(null, null, auth))
    .thenReturn(“/anyTargetUrl”);

    // exercise test
    final String determinedTargetUrl = roleBasedTargetUrlResolver
    .determineTargetUrl(null, null, auth);

    // behavior verification
    verify(fallbackTargetUrlResolver, times(1)).determineTargetUrl(null,
    null, auth);

    // state verification
    assertThat(determinedTargetUrl, is(“/anyTargetUrl”));
    }
    }

    • heraclitusonsoftware Says:

      Thanks Christian, my preference now is for using mockito over jmock and if i was rewriting it again I would use mockito.

  6. Shalom Says:

    Hi,
    It has been a while since you wrote this page, so maybe my comment is no longer required, but just in case, since I have seen this page quoted in a few places and you helped me get to working code:

    There is (at least now) a way that I think is a lot easier:
    Instead of deleting

    change the default-target-url=”/home.htm” to something like authentication-success-handler-ref=”myAuthenticationSuccessHandler” where
    “myAuthenticationSuccessHandler” is a Bean that implements “AuthenticationSuccessHandler” .

    There is one required method
    public void onAuthenticationSuccess(HttpServletRequest request,
    HttpServletResponse response,
    Authentication authentication)
    throws IOException, ServletException {

    response.sendRedirect(getTheDesiredTargetURL(request, authentication));
    }

    As I said, without your site I would not have found the above solution. So thanks for your help.

  7. Kalim Says:

    Can some body explain me what is the value 1 and 0 in both roles ADMIN_ROLE(“ROLE_ADMIN”, 1), USER_ROLE(“ROLE_USER”, 0); This is when I have one Admin role which also contain user role and one user role. What if I have more that 2 roles for admin role e.g. Admin, supervisor, user etc..? what will be the # in admin role will be?

  8. tv amr Says:

    Thanks for this interesting post. I will likely be certain to tell others about this site :) Excellent post. Cant wait to check out your next post.

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: