11

In my Spring boot app, I have bunch of endpoints at /api/**. The following is my App configuration:

@Configuration
public class AppConfig extends WebMvcConfigurerAdapter {

    private class PushStateResourceResolver implements ResourceResolver {
        private Resource index = new ClassPathResource("/public/index.html");
        private List<String> handledExtensions = Arrays.asList("html", "js",
                "json", "csv", "css", "png", "svg", "eot", "ttf", "woff",
                "appcache", "jpg", "jpeg", "gif", "ico");

        private List<String> ignoredPaths = Arrays.asList("^api\\/.*$");

        @Override
        public Resource resolveResource(HttpServletRequest request,
                String requestPath, List<? extends Resource> locations,
                ResourceResolverChain chain) {
            return resolve(requestPath, locations);
        }

        @Override
        public String resolveUrlPath(String resourcePath,
                List<? extends Resource> locations, ResourceResolverChain chain) {
            Resource resolvedResource = resolve(resourcePath, locations);
            if (resolvedResource == null) {
                return null;
            }
            try {
                return resolvedResource.getURL().toString();
            } catch (IOException e) {
                return resolvedResource.getFilename();
            }
        }

        private Resource resolve(String requestPath,
                List<? extends Resource> locations) {
            if (isIgnored(requestPath)) {
                return null;
            }
            if (isHandled(requestPath)) {
                return locations
                        .stream()
                        .map(loc -> createRelative(loc, requestPath))
                        .filter(resource -> resource != null
                                && resource.exists()).findFirst()
                        .orElseGet(null);
            }
            return index;
        }

        private Resource createRelative(Resource resource, String relativePath) {
            try {
                return resource.createRelative(relativePath);
            } catch (IOException e) {
                return null;
            }
        }

        private boolean isIgnored(String path) {
            return false;
            //          return !ignoredPaths.stream().noneMatch(rgx -> Pattern.matches(rgx, path));
            //deliberately made this change for examining the code
        }

        private boolean isHandled(String path) {
            String extension = StringUtils.getFilenameExtension(path);
            return handledExtensions.stream().anyMatch(
                    ext -> ext.equals(extension));
        }
    }
}

The access to the endpoints behind /api/** is checked to be authenticated, therefore when I type in /api/my_endpoint in the browser, I get 401 error back, which is not what I want. I want users to be served with index.html.

Arian
  • 7,397
  • 21
  • 89
  • 177
  • 1
    You can use an interceptor or a filter for all request, and check response & path and act accordly, dont you? – DvTr Dec 13 '17 at 02:08

3 Answers3

4

You can check for the X-Requested-With header:

private boolean isAjax(HttpServletRequest request) {
    String requestedWithHeader = request.getHeader("X-Requested-With");
    return "XMLHttpRequest".equals(requestedWithHeader);
}

UPDATE: Maybe it's a better approach to check for the Accept header. I think the probability is much higher that browsers include a Accept: text/html header than scripts etc. include a X-Requested-With header.

You could create a custom authentication entry point and redirect the user if the Accept: text/html header is present:

public class CustomEntryPoint implements AuthenticationEntryPoint {

    private static final String ACCEPT_HEADER = "Accept";

    private final RedirectStrategy redirect = new DefaultRedirectStrategy();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        if (isHtmlRequest(request)) {
            redirect.sendRedirect(request, response, "/");
        } else {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access is not allowed");
        }
    }

    private boolean isHtmlRequest(HttpServletRequest request) {
        String acceptHeader = request.getHeader(ACCEPT_HEADER);
        List<MediaType> acceptedMediaTypes = MediaType.parseMediaTypes(acceptHeader);
        return acceptedMediaTypes.contains(MediaType.TEXT_HTML);
    }

}

Note:

If you use a custom authentication filter (inherited from AbstractAuthenticationProcessingFilter) then the authentication entry point won't be called. You can handle the redirect in the unsuccessfulAuthentication() method of AbstractAuthenticationProcessingFilter.

Alternatives:

  • Override standard BasicErrorController of Spring Boot and handle redirect of 401 Unauthorized errors there.
  • Why not just return JSON on all /api calls and html otherwise?
Christian
  • 295
  • 2
  • 6
2

So, I finally resolved this issue by fixing my security config:

I have a custom JWTAuthenticationFilter in which I override the unsuccessfulAuthentication method:

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    logger.debug("failed authentication while attempting to access "+ URL_PATH_HELPER.getPathWithinApplication((HttpServletRequest) request));
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.sendRedirect("/");
}

As you can see, if authentication fails, I redirect the user to the "/" which in return will be captured by the resource resolver and index.html will be served !

Arian
  • 7,397
  • 21
  • 89
  • 177
  • Nice that you have solved your problem. Please give more info next time, e.g. that you use a custom authentication filter. – Christian Dec 20 '17 at 08:08
1

Browsers commonly set the "User-Agent" header of the http request.

So you can distinguish these calls using: request.getHeader("User-Agent");

Also see:

tkruse
  • 10,222
  • 7
  • 53
  • 80