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.
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)
....;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With