The new Certification Class of Learn Spring Security is out:

>> CHECK OUT THE COURSE

1. Overview

This article will focus on how to redirect a user back to the originally requested URL – after they log in.

Previously, we’ve seen how to redirect to different pages after login with Spring Security for different types of users and covered various types of redirections with Spring MVC.

The article is based on top of the Spring Security Login tutorial.

2. Common Practice

The most common ways to implement redirection logic after login are:

  • using HTTP Referer header
  • saving the original request in the session
  • appending original URL to the redirected login URL

Using the HTTP Referer header is a straightforward way, for most browsers and HTTP clients set Referer automatically. However, as Referer is forgeable and relies on client implementation, using HTTP Referer header to implement redirection is generally not suggested.

Saving the original request in the session is a safe and robust way to implement this kind of redirect. Besides the original URL, we can store original request attributes and any custom properties in the session.

And appending original URL to the redirected login URL is usually seen in SSO implementations. When authenticated via an SSO service, users will be redirected to the originally requested page, with the URL appended. We must ensure the appended URL is properly encoded.

Another similar implementation is to put the original request URL in a hidden field inside the login form. But this is no better than using HTTP Referer

In Spring Security, the first two approaches are natively supported.

3. AuthenticationSuccessHandler

In form-based authentication, redirection happens right after login, which is handled in an AuthenticationSuccessHandler instance in Spring Security.

Three default implementations are provided: SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler and ForwardAuthenticationSuccessHandler. We’ll focus on the first two implementations.

3.1. SavedRequestAwareAuthenticationSuccessHandler

SavedRequestAwareAuthenticationSuccessHandler makes use of the saved request stored in the session. After a successful login, users will be redirected to the URL saved in the original request.

For form login, SavedRequestAwareAuthenticationSuccessHandler is used as the default AuthenticationSuccessHandler.

@Configuration
@EnableWebSecurity
public class RedirectionSecurityConfig extends WebSecurityConfigurerAdapter {

    //...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests()
          .antMatchers("/login*")
          .permitAll()
          .anyRequest()
          .authenticated()
          .and()
          .formLogin();
    }
    
}

And the equivalent XML would be:

<http>
    <intercept-url pattern="/login" access="permitAll"/>
    <intercept-url pattern="/**" access="isAuthenticated()"/>
    <form-login />
</http>

Suppose we have a secured resource at location “/secured”. For the first time access to the resource, we’ll be redirected to the login page; after filling in credentials and posting the login form, we’ll be redirected back to our originally requested resource location:

@Test
public void givenAccessSecuredResource_whenAuthenticated_thenRedirectedBack() 
  throws Exception {
 
    MockHttpServletRequestBuilder securedResourceAccess = get("/secured");
    MvcResult unauthenticatedResult = mvc
      .perform(securedResourceAccess)
      .andExpect(status().is3xxRedirection())
      .andReturn();

    MockHttpSession session = (MockHttpSession) unauthenticatedResult
      .getRequest()
      .getSession();
    String loginUrl = unauthenticatedResult
      .getResponse()
      .getRedirectedUrl();
    mvc
      .perform(post(loginUrl)
        .param("username", userDetails.getUsername())
        .param("password", userDetails.getPassword())
        .session(session)
        .with(csrf()))
      .andExpect(status().is3xxRedirection())
      .andExpect(redirectedUrlPattern("**/secured"))
      .andReturn();

    mvc
      .perform(securedResourceAccess.session(session))
      .andExpect(status().isOk());
}

3.2. SimpleUrlAuthenticationSuccessHandler

Compared to the SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationSuccessHandler gives us more options on redirection decisions.

We can enable Referer-based redirection by setUserReferer(true):

public class RefererRedirectionAuthenticationSuccessHandler 
  extends SimpleUrlAuthenticationSuccessHandler
  implements AuthenticationSuccessHandler {

    public RefererRedirectionAuthenticationSuccessHandler() {
        super();
        setUseReferer(true);
    }

}

Then use it as the AuthenticationSuccessHandler in RedirectionSecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
      .antMatchers("/login*")
      .permitAll()
      .anyRequest()
      .authenticated()
      .and()
      .formLogin()
      .successHandler(new RefererAuthenticationSuccessHandler());
}

And for XML configuration:

<http>
    <intercept-url pattern="/login" access="permitAll"/>
    <intercept-url pattern="/**" access="isAuthenticated()"/>
    <form-login authentication-success-handler-ref="refererHandler" />
</http>

<beans:bean 
  class="RefererRedirectionAuthenticationSuccessHandler" 
  name="refererHandler"/>

3.3. Under the Hood

There is no magic in these easy to use features in Spring Security. When a secured resource is being requested, the request will be filtered by a chain of various filters. Authentication principals and permissions will be checked. If the request session is not authenticated yet, AuthenticationException will be thrown.

The AuthenticationException will be caught in the ExceptionTranslationFilterin which an authentication process will be commenced, resulting in a redirection to the login page.

public class ExceptionTranslationFilter extends GenericFilterBean {

    //...

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
        //...

        handleSpringSecurityException(request, response, chain, ase);

        //...
    }

    private void handleSpringSecurityException(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, RuntimeException exception)
      throws IOException, ServletException {

        if (exception instanceof AuthenticationException) {

            sendStartAuthentication(request, response, chain,
              (AuthenticationException) exception);

        }

        //...
    }

    protected void sendStartAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
       
       SecurityContextHolder.getContext().setAuthentication(null);
       requestCache.saveRequest(request, response);
       authenticationEntryPoint.commence(request, response, reason);
    }

    //... 

}

After login, we can customize behaviors in an AuthenticationSuccessHandler, as shown above.

4. Conclusion

In this Spring Security example, we discussed common practice for redirection after login and explained implementations using Spring Security.

Note that all the implementations we mentioned are vulnerable to certain attacks if no validation or extra method controls are applied. Users might be redirected to a malicious site by such attacks.

The OWASP has provided a cheat sheet to help us handle unvalidated redirects and forwards. This would do a lot of help if we need to build implementations on our own.

The full implementation code of this article can be found over on Github.

Go deeper into Spring Security with the course:

>> LEARN SPRING SECURITY