CSRF Cross Domain

Viewed 3036

My REST API backend currently uses a cookie based CSRF protection.

The basic process is that the backend sets a cookie that can be read by a client application and then on subsequent HXR requests (that are permitted by my CORS settings) a custom header is passed along with the cookie and the server checks the two values match.

Essentially it's all enabled with one pretty simple line of Java code in spring security.

.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())

This works great when the UI is served from the same domain since the JS in the client can easily access the (non-http-only) cookie to read the value and send the custom header.

My challenge comes when I want my client app to be deployed on a different domain, e.g.

API: api.x.com
UI: ui.y.com

My idea to solve this is

  1. Instead of sending the token back in just a cookie, it could be sent back in a custom response header, also along with the cookie.
  2. The client then reads the custom header and locally sores (either using local storage or perhaps by dynamically creating a cookie on the client side, but this time on the UI domain so that it can read it later).
  3. This value is subsequently then used by the client when it makes XHR requests in a custom request header and the cookie that was set in step 1 will also go along with it.
  4. The server checks that these two values (cookie and request header) are set and that they match exactly.

Is this a well known/acceptable approach? Can anyone identify any obvious flaws with this approach from a security perspective.

Obviously the API server will need to allow CORS for the UI domain + allow credentials and expose the custom response header in the CORS policy.

Edit

I am going to attempt to achieve this in Spring Security using this custom repository that I have written:

import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * This class is essentially a wrapper for a cookie based CSRF protection scheme.
 * <p>
 * The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
 * <p>
 * This mechanism essentially does the same thing, but also provides a response header so that the client can read this value and the use some local mechanism to store the token (session storage, local storage, local user agent DB, construct a new cookie on the UI domain etc).
 */
public class CrossDomainHeaderAndCookieCsrfTokenRepository implements CsrfTokenRepository {

    public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
    private static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
    private static final String CSRF_QUERY_PARAM_NAME = "_csrf";

    private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();

    public CrossDomainHeaderAndCookieCsrfTokenRepository() {
        delegate.setCookieHttpOnly(true);
        delegate.setHeaderName(XSRF_HEADER_NAME);
        delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
        delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
    }

    @Override
    public CsrfToken generateToken(final HttpServletRequest request) {
        return delegate.generateToken(request);
    }

    @Override
    public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
        delegate.saveToken(token, request, response);
        response.setHeader(token.getHeaderName(), token.getToken());
    }

    @Override
    public CsrfToken loadToken(final HttpServletRequest request) {
        return delegate.loadToken(request);
    }
}
2 Answers

I think you are able to provide another implementation for the CsrfTokenRepository to support different domain pattern for the CSRF token.

You can clone the original implementation with the following changes to the code:

....

private String domain;
private Pattern domainPattern;

....

public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {

    ....

    String domain = getDomain(request);
    if (domain != null) {
        cookie.setDomain(domain);
    }

    response.addCookie(cookie);
}

.....    

public void setDomainPattern(String domainPattern) {
    if (this.domain != null) {
        throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
    }
    this.domainPattern = Pattern.compile(domainPattern, Pattern.CASE_INSENSITIVE);
}

public void setDomain(String domain) {
    if (this.domainPattern != null) {
        throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
    }
    this.domain = domain;
}

private String getDomain(HttpServletRequest request) {
    if (this.domain != null) {
        return this.domain;
    }
    if (this.domainPattern != null) {
        Matcher matcher = this.domainPattern.matcher(request.getServerName());
        if (matcher.matches()) {
            return matcher.group(1);
        }
    }
    return null;
}

Then, provide with with your new implementation.

.csrf().csrfTokenRepository(new CustomCookieCsrfTokenRepository())

I've been successfully using a class similar to the one in my description edit in production for about 1 year now. The class is:

/**
 * This class is essentially a wrapper for a cookie based CSRF protection scheme.
 * The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then
 * the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
 * This mechanism does the same thing, but also provides a response header so that the client can read this value and the use
 * some local mechanism to store the token (local storage, local user agent DB, construct a new cookie on the UI domain etc).
 *
 * @see <a href="https://stackoverflow.com/questions/45424496/csrf-cross-domain">https://stackoverflow.com/questions/45424496/csrf-cross-domain</a>
 */
public class CrossDomainCsrfTokenRepository implements CsrfTokenRepository {

    public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
    public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
    private static final String CSRF_QUERY_PARAM_NAME = "_csrf";

    private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();

    public CrossDomainCsrfTokenRepository() {
        delegate.setCookieHttpOnly(true);
        delegate.setHeaderName(XSRF_HEADER_NAME);
        delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
        delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
    }

    @Override
    public CsrfToken generateToken(final HttpServletRequest request) {
        return delegate.generateToken(request);
    }

    @Override
    public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
        delegate.saveToken(token, request, response);
        response.setHeader(XSRF_HEADER_NAME, nullSafeTokenValue(token));
    }

    @Override
    public CsrfToken loadToken(final HttpServletRequest request) {
        return delegate.loadToken(request);
    }

    private String nullSafeTokenValue(final CsrfToken token) {
        return ofNullable(token)
            .map(CsrfToken::getToken)
            .orElse("");
    }
}

And I enable it via the spring boot security config:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CsrfTokenRepository csrfTokenRepository;

    @Override
    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
    protected void configure(final HttpSecurity http) throws Exception {
        http.csrf().ignoringAntMatchers(CTM_RESOURCE).csrfTokenRepository(csrfTokenRepository);
    }

}

Be aware that I also did enable a CORS property source bean to the WebSecurityConfig class shown in this post to whitelist the relevant XSRF headers:

@Bean
    public UrlBasedCorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(properties.getAllowedOrigins());
        configuration.setAllowedMethods(allHttpMethods());
        configuration.setAllowedHeaders(asList(CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME, CONTENT_TYPE));
        configuration.setExposedHeaders(asList(LOCATION, CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(HOURS.toSeconds(properties.getMaxAgeInHours()));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
Related