Robin Hermans Robin Hermans - 1 month ago 21
Java Question

Spring Security: How to add a redirect query parameter to the login url to allow bookmarking of the page?

Problem Scenario

I'm currently working on the login page of my application which is based on Spring boot and related Spring projects (like security and cloud). I expect the users of my application to bookmark the login page and thus need to act of this behavior. As I started thinking about potential problems I figured that the application would not be able to know where to redirect to (since this can be multiple urls) after you bookmark the page (since there is only /login and no redirect what so ever). Normally the user would not to, for example, /dashboard and gets redirected to login cause there is no authentication present. After the use presents his or her credentials the application redirect the user. But that's only possible cause the application holds a SavedRequest in his current session telling the redirect location.

What I want to achieve

Basically what I want to achieve is for the application to know where to go to after the user has set a bookmark on the /login url. The ideal situation here would be for the /login url to contain a redirect parameter. For example.


  1. User visits /dashboard?param=value

  2. No authentication present, Application redirects to /login?redirect=/dashboard?param=value

  3. User logs in

  4. Application sends user to /dashboard?param=value.



Now if the user would bookmark the url served in step 2, then upon clicking the bookmark after some time, would supply the application with enough information to make a sensible redirect.

In case someone knows a better approach I'd like to hear it.

Steps taken so far

Until now I've based my search for a solution on another answer on StackOverflow. It seems like a step in the right direction but some of the desired functionality is still missing.

I started out by creating a Custom implementation of the LoginUrlAuthenticationEntryPoint class. It overrides the commence method, which looks like this:

public class CustomLoginUrlAuthenticaitonEntryPoint extends LoginUrlAuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
{
if (!request.getRequestURI().equals(this.getLoginFormUrl()))
{
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
redirectStrategy.sendRedirect(request, response, getLoginFormUrl() + "?redirect=" + request.getRequestURI() + "?" + request.getQueryString());
}
}
}


Then I added this custom class to the HttpSecurity as the default authentication entry point.

@Configuration
@Order(-20)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomLoginUrlAuthenticationEntryPoint("/login"));
}
}


And last I implemented a custom login controller to server the Login page.

@Controller
public class LoginController
{
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView login(@RequestParam(value = "redirect", required = false) String redirect)
{
ModelAndView model = new ModelAndView();
// Do something with the redirect url;
model.setViewName("login");
return model;
}


But once I implemented this it seemed that the redirect was working correctly. (/dashboard?param=value was redirected to /login?redirect=/dashboard?param=value) but the login page was not showing. But when visiting the /login url directly the login page does show.

So I think I'm in the right place for adding a custom query parameter to the /login url, but the implementation is not quite complete I guess. Can someone help me figure things out, or maybe supply a better solution for my problem?

Thanks in advance.

Answer

WARNING: Using a parameter to determine where you are redirecting to can open your application up to Open Redirect Vulnerabilities. Be very cautions when performing redirects based upon user input.

ContinueEntryPoint

Your first step is to create an AuthenticationEntryPoint which is in charge of including a parameter with the URL to continue to in the URL when displaying the log in form. In this example, we will use the parameter name continue.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * @author Rob Winch
 *
 */
public class ContinueEntryPoint extends LoginUrlAuthenticationEntryPoint {

    public ContinueEntryPoint(String loginFormUrl) {
        super(loginFormUrl);
    }

    @Override
    protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) {

        String continueParamValue = UrlUtils.buildRequestUrl(request);
        String redirect = super.determineUrlToUseForThisRequest(request, response, exception);
        return UriComponentsBuilder.fromPath(redirect).queryParam("continue", continueParamValue).toUriString();
    }
}

WebSecurityConfig

The next step is to include a Security configuration that uses the ContinueEntryPoint. For example:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

LoginController

Finally, you should create a LoginController that redirects to the parameter if the user is already authenticated. For example:

import javax.validation.constraints.Pattern;

import org.hibernate.validator.constraints.NotBlank;

public class RedirectModel {
    @Pattern(regexp="^/([^/].*)?$")
    @NotBlank
    private String continueUrl;

    public void setContinue(String continueUrl) {
        this.continueUrl = continueUrl;
    }

    public String getContinue() {
        return continueUrl;
    }
}

@Controller
public class LoginController {

    @RequestMapping("/login")
    public String login(Principal principal, @Valid @ModelAttribute RedirectModel model, BindingResult result) {
        if (!result.hasErrors() && principal != null) {
            // do not redirect for absolute URLs (i.e. https://evil.com)
            // do not redirect if we are not authenticated
            return "redirect:" + model.getContinue();
        }
        return "login";
    }
}

Complete Sample

You can find a complete sample in github at rwinch/spring-security-sample in the so-34087954-continue-on-login branch. You can easily download it if you prefer not to use git.

Comments