JAX-RS: Enabling Cross-Origin Resource Sharing (CORS)

One of the common difficulties backend development teams may face with when creating RESTful services is a Cross-origin resource sharing (CORS). Since JAX-RS does not allow CORS by default, we are going to implement a solution that enables it.

While you are hosting back-end and front-end resources on the same machine (e.g. localhost) there won’t be a problem, but when it comes to a cross domain deployment (for instance, when front-end resources hosted on different machine) some proper solution is required.

Implementing CORS Feature

In order to enable CORS feature in JAX-RS we need to add the following RestEasy JAX-RS dependency:

<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-jaxrs</artifactId>
  <version>3.1.0.Final</version>
</dependency>

It provides convenient CorsFilter, which we can add as a feature:

import javax.ws.rs.core.FeatureContext;  
import org.jboss.resteasy.plugins.interceptors.CorsFilter;

@Provider
public class CorsProvider implements Feature {

    @Override
    public boolean configure(FeatureContext context) {
        CorsFilter corsFilter = new CorsFilter();
        corsFilter.getAllowedOrigins().add("*");
        context.register(corsFilter);
        return true;
    }

As you may notice, we are adding * to allowed origins. This will add all the appropriate CORS headers to any outgoing request.

Implementing CORS Filter

If you do not want to add additional dependencies for such simple case, you can implement CORS Filter as follows:

@PreMatching
public class CorsFilter implements ContainerRequestFilter, ContainerResponseFilter {

    private static final String HEADER_ORIGIN = "Origin";

    private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

    private static final String HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";

    private static final String HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";

    private static final String HEADER_ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";

    private static final String HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";

    private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";

    private Set<String> allowedOrigins = new HashSet<>();

    public Set<String> getAllowedOrigins() {
        return allowedOrigins;
    }

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String origin = requestContext.getHeaderString(HEADER_ORIGIN);
        if (origin == null) {
            return;
        }
        if (requestContext.getMethod().equalsIgnoreCase("OPTIONS")) {
            preFlight(origin, requestContext);
        } else {
            checkOrigin(requestContext, origin);
        }
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        String origin = requestContext.getHeaderString(HEADER_ORIGIN);
        if (origin == null
                || requestContext.getMethod().equalsIgnoreCase("OPTIONS")
                || requestContext.getProperty("cors.failure") != null) {
            return;
        }
        responseContext.getHeaders().putSingle(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
        responseContext.getHeaders().putSingle(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    }


    private void preFlight(String origin, ContainerRequestContext requestContext) throws IOException {
        checkOrigin(requestContext, origin);
        Response.ResponseBuilder builder = Response.ok();
        builder.header(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
        builder.header(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");

        String requestMethods = requestContext.getHeaderString(HEADER_ACCESS_CONTROL_REQUEST_METHOD);
        if (requestMethods != null) {
            builder.header(HEADER_ACCESS_CONTROL_ALLOW_METHODS, requestMethods);
        }

        String allowHeaders = requestContext.getHeaderString(HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
        if (allowHeaders != null) {
            builder.header(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
        }
        requestContext.abortWith(builder.build());
    }

    private void checkOrigin(ContainerRequestContext requestContext, String origin) {
        if (!allowedOrigins.contains("*")
                && !allowedOrigins.contains(origin)) {
            requestContext.setProperty("cors.failure", true);
            throw new ForbiddenException("Origin not allowed: " + origin);
        }
    }
}

This is almost the same implementation as org.jboss.resteasy.plugins.interceptors.CorsFilter, but without necessity to import additional dependencies.

Allowing HTTP OPTIONS Method

The last thing to consider is to allow HTTP OPTIONS method to proceed. HTTP OPTIONS method is usually used by browsers before sending the actual CORS request. 

If you have security constraints configured in web.xml descriptor file, you need to add HTTP OPTIONS method using the following whitelist declaration:

<security-constraint>
  <web-resource-collection>
    <url-pattern>/*</url-pattern>
    <http-method>OPTIONS</http-method>
  </web-resource-collection>
</security-constraint> 

Enjoy your day and read more backend development articles here!

Vladyslav Baidak - Scalified

Vladyslav Baidak

Backend Engineer at Scalified