Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring - Redirect to a link upon successfully logging in, after a failed Ajax request

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.

like image 942
cavpollo Avatar asked Nov 07 '22 00:11

cavpollo


1 Answers

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)
            ....;
}
like image 198
cavpollo Avatar answered Nov 15 '22 00:11

cavpollo