Monday, December 18, 2017

Spring to distinguish browser visitors from API calls to the endpoints

Leave a Comment

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.

3 Answers

Answers 1

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);     }  } 

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?

Answers 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 !

Answers 3

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

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

Also see:

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment