5

I have a website that requires some HTML to be rendered inside an element asynchronously upon an user action. If the user's session expires things get tricky, but it can be solved by creating a custom AuthenticationEntryPoint class like this SO question and this SO question suggest.

My problem comes once the user logs back in because the user gets redirected to the last URL that was requested, which happens to be the Ajax request, therefore my user gets redirected to a fragment of an HTML, instead of the last page it browsed.

I was able to solve this by removing a session attribute on the custom AuthenticationEntryPoint:

if (ajaxOrAsync) {
    request.getSession().removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}

Here comes my question's problem.

While the previous code solves my issue, it has the side effect of redirecting the user to the home page instead of the last page it browsed (as there is no saved request). It wouldn't be much of a problem, but it makes the website inconsistent because if the last request was an asynchronous request, it gets redirected home but if it was a normal request it gets redirected to the last page browsed. =(

I managed to code this to handle that scenario:

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.PortResolver;
import org.springframework.security.web.PortResolverImpl;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.apache.commons.lang.StringUtils.isBlank;

public class CustomAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
    ... // Some not so relevant code

    @Override
    public void commence(final HttpServletRequest request,
                         final HttpServletResponse response,
                         final AuthenticationException authException) throws IOException, ServletException {
        ... // some code to determine if the request is an ajax request or an async one
        if (ajaxOrAsync) {
            useRefererAsSavedRequest(request);

            response.sendError(SC_UNAUTHORIZED);
        } else {
            super.commence(request, response, authException);
        }
    }

    private void useRefererAsSavedRequest(final HttpServletRequest request) {
        request.getSession().removeAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE);

        final URL refererUrl = getRefererUrl(request);
        if (refererUrl != null) {
            final HttpServletRequestWrapper newRequest = new CustomHttpServletRequest(request, refererUrl);
            final PortResolver portResolver = new PortResolverImpl();
            final DefaultSavedRequest newSpringSecuritySavedRequest = new DefaultSavedRequest(newRequest, portResolver);

            request.getSession().setAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE, newSpringSecuritySavedRequest);
        }
    }

    private URL getRefererUrl(final HttpServletRequest request) {
        final String referer = request.getHeader("referer");

        if (isBlank(referer)) {
            return null;
        }

        try {
            return new URL(referer);
        } catch (final MalformedURLException exception) {
            return null;
        }
    }

    private class CustomHttpServletRequest extends HttpServletRequestWrapper {
        private URL url;

        public CustomHttpServletRequest(final HttpServletRequest request, final URL url) {
            super(request);
            this.url = url;
        }

        @Override
        public String getRequestURI() {
            return url.getPath();
        }

        @Override
        public StringBuffer getRequestURL() {
            return new StringBuffer(url.toString());
        }

        @Override
        public String getServletPath() {
            return url.getPath();
        }

    }
}

The previous code solves my issue, but it is a very hacky approach to solve my redirection problem (I cloned and overwrote the original request... +shudders+).

So my question is, Is there any other way to rewrite the link that Spring uses to redirect the user after a successful login (given the conditions I'm working with)?

I've looked at Spring's AuthenticationSuccessHandler, but I haven't found a way of communicating the referer url to it in case of a failed Ajax request.

cavpollo
  • 4,071
  • 2
  • 40
  • 64
  • Can I confirm, here you have various AJAX requests going on and it's possible that some AJAX response could come back as `HTTP 401 unauthorized`? If so, rather than solving this server side, can you now solve this client side by having a global AJAX error handler function which upon `http 401` response would pop up some sort of login dialog and on successful login could potentially refresh the current page? – David Oct 29 '18 at 16:25
  • @DavidGoate Yes, the ajax responses come back as 401. A notification is shown indicating the user that he isn't logged in. Showing a modal to login again is a good solution. I'll see if the project requirements can be tweaked to accommodate for this solution. – cavpollo Oct 29 '18 at 17:50
  • You wouldn't necessarily need to follow the modal solution, it was just one suggestion. Perhaps another option is that upon detecting the 401 you app captures the window location using javascript and then you redirect to a login form with a query param such as `https://my-site.com/login?continue={windowLocation}`. You can then have your login filter use this parameter to forward onto the correct place after successful authentication and then use new `SimpleUrlAuthenticationSuccessHandler().setTargetUrlParameter("continue");` – David Oct 30 '18 at 17:27
  • Thanks @DavidGoate, this sounds good too. For now I'll stick to the custom `ExceptionTranslationFilter`, but if it proves unreliable I'll give your good suggestions a try. =) – cavpollo Oct 30 '18 at 18:31

1 Answers1

1

I've found an acceptable solution to my problem thanks to an idea that came up when reading the docs and later on browsing this other SO answer. In short, I would have to create my own custom ExceptionTranslationFilter, and override the sendStartAuthentication to not to save the request cache.

If one takes a look at the ExceptionTranslationFilter code, it looks this (for Finchley SR1):

protected void sendStartAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain,
        AuthenticationException reason) throws ServletException, IOException {
    SecurityContextHolder.getContext().setAuthentication(null);
    requestCache.saveRequest(request, response); // <--- Look at me
    logger.debug("Calling Authentication entry point.");
    authenticationEntryPoint.commence(request, response, reason);
}

So, to not save data from Ajax requests I should implement an CustomExceptionTranslationFilter that acts like this:

@Override
protected void sendStartAuthentication(final HttpServletRequest request,
                                       final HttpServletResponse response,
                                       final FilterChain chain,
                                       final AuthenticationException authenticationException) throws ServletException, IOException {
    ... // some code to determine if the request is an ajax request or an async one
    if (isAjaxOrAsyncRequest) {
        SecurityContextHolder.getContext().setAuthentication(null);

        authenticationEntryPoint.commence(request, response, authenticationException);
    } else {
        super.sendStartAuthentication(request, response, chain, authenticationException);
    }
}

This makes the CustomAuthenticationEntryPoint logic much simpler:

@Override
public void commence(final HttpServletRequest request,
                     final HttpServletResponse response,
                     final AuthenticationException authException) throws IOException, ServletException {
    ... // some code to determine if the request is an ajax request or an async one, again
    if (isAjaxOrAsyncRequest) {
        response.sendError(SC_UNAUTHORIZED);
    } else {
        super.commence(request, response, authException);
    }
}

And my CustomWebSecurityConfigurerAdapter should be configured like this:

@Override
protected void configure(final HttpSecurity http) throws Exception {
    final CustomAuthenticationEntryPoint customAuthenticationEntryPoint =
            new CustomAuthenticationEntryPoint("/login-path");

    final CustomExceptionTranslationFilter customExceptionTranslationFilter =
            new CustomExceptionTranslationFilter(customAuthenticationEntryPoint);

    http.addFilterAfter(customExceptionTranslationFilter, ExceptionTranslationFilter.class)
            ....
            .permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(customAuthenticationEntryPoint)
            ....;
}
cavpollo
  • 4,071
  • 2
  • 40
  • 64