1

I'm confused as to how should I do CSRF protection. I have separate frontend (angularjs) and backend (Spring). They are deployed in completely separate places, and are communicating by REST.

My problem is as follows. Angular refuses to send my CSRF cookie cross domain - all I can send is CSRF header. I've tried adding withCredentials to both angular and CORS filter on my backend and setting up xsrf header and cookie as described here under Usage.

Any ideas what could I be doing wrong? If you want some specific part of my code, please post and I'll deliver.

@Adding relevant code:

CORSFilter

public class CORSFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:9000");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,origin,content-type,accept,X-XSRF-TOKEN, authorization, customer-id, X-AUTH-TOKEN");
        response.setHeader("Access-Control-Expose-Headers", "employee_name, employee_id, employee_customer_id, X-AUTH-TOKEN");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        if (request.getMethod()!="OPTIONS") {
            chain.doFilter(req, res);
        } else {
        }
    }

CSRF Filter

public class StatelessCSRFFilter extends OncePerRequestFilter {

    private static final String CSRF_TOKEN = "CSRF-TOKEN";
    private static final String X_CSRF_TOKEN = "X-XSRF-TOKEN";
    private final RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher();
    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (requireCsrfProtectionMatcher.matches(request)) {
            final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
            final Cookie[] cookies = request.getCookies();

            String csrfCookieValue = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(CSRF_TOKEN)) {
                        csrfCookieValue = cookie.getValue();
                    }
                }
            }
            if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
                accessDeniedHandler.handle(request, response, new AccessDeniedException(
                        "Missing or non-matching CSRF-token"));
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    public static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");

        @Override
        public boolean matches(HttpServletRequest request) {
            return !allowedMethods.matcher(request.getMethod()).matches();
        }
    }

app.js

(...)
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';
$httpProvider.interceptors.push('InterceptorCsrf');
$httpProvider.defaults.withCredentials = true;
(...)

InterceptorCsrf.js

angular.module('EnterprisePortalApp')
        .factory('InterceptorCsrf',function($cookies, $cookieStorage){
            function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e16]+1e16).replace(/[01]/g,b)};
            return {
                //With each request generate new csrf token
                request: function(config) {
                    $cookieStorage.put("CSRF-TOKEN", b());
                    config.headers['X-XSRF-TOKEN'] = $cookies.get('CSRF-TOKEN');
                    return config;
                }
            }   
});
İlker Korkut
  • 3,129
  • 3
  • 30
  • 51
Krzysztof Piszko
  • 1,355
  • 3
  • 12
  • 17

2 Answers2

1

Your code blocks seems ok. Have you tried "Access-Control-Allow-Origin", "http://localhost:9000" change to this *.

BTW it's a bug in chrome pointing localhost with its port which won't fix(SO discussion).

Also you can try to give different domain names(instead of localhost you may use nginx proxy settings etc. it may be some tricky) both rest server and client-side hosts.

Extra Info according to that situation:

If using token based authentication for your REST service, you don't need to implement csrf protection additionally.

If user need to send his access token(eg. jwt) on every request for this rest service, your service is protected against csrf, and also similar method with csrf protection. User gets token->request messages with token->decode token on backend->getuserid(basic) and make his process the token based request process like these. In this scenario if user doesn't have token, he can't do anyhing.

Community
  • 1
  • 1
İlker Korkut
  • 3,129
  • 3
  • 30
  • 51
  • I can't use `response.setHeader("Access-Control-Allow-Credentials", "true");` and `"Access-Control-Allow-Origin", "http://localhost:9000"` at the same time. ` A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true.` gets thrown – Krzysztof Piszko Aug 26 '15 at 14:30
  • Do you use `X-AUTH-TOKEN` in every request? If so you don't need csrf token. BTW did you try to give specific domain to both front-end host and rest service host? – İlker Korkut Aug 26 '15 at 15:10
  • Would you explain why I don't need csrf token? – Krzysztof Piszko Aug 27 '15 at 07:04
  • @KrzysztofPiszko If user need to send his access token(eg. jwt) on every request for this rest service, your service is protected against csrf, and also similar method with csrf protection. `User gets token->request messages with token->decode token on backend->getuserid and make his process` the token based request process like these. If you have this spec so don't waste your time to implement csrf protection, in this scenario if user doesn't have token, he can't do anyhing. – İlker Korkut Aug 27 '15 at 07:19
  • Got it, I didn't realize that it would be as simple as that. Now that I've reread what csrf is about it seems to fit. Thanks! – Krzysztof Piszko Aug 27 '15 at 07:34
  • @KrzysztofPiszko I'm updating my answer with this extra information, please don't forget to give feedback if answer is useful for you. – İlker Korkut Aug 27 '15 at 07:57
-1

Working for me, with the following code

  1. Server side, I have coded the CSRF and CORS filters as described in the official Spring Angular guide.
  2. Client side, I had to code an $http interceptor as below, because AngularJS doesn't automatically add the header for cross-domain requests.

    angular.module('appBoot')
      .factory('XSRFInterceptor', function ($cookies, $log) {
    
        var XSRFInterceptor = {
    
          request: function(config) {
    
            var token = $cookies.get('XSRF-TOKEN');
    
            if (token) {
              config.headers['X-XSRF-TOKEN'] = token;
              $log.info("X-XSRF-TOKEN: " + token);
            }
    
            return config;
          }
        };
        return XSRFInterceptor;
      });
    
    angular.module('appBoot', ['ngCookies', 'ngMessages', 'ui.bootstrap', 'vcRecaptcha'])
        .config(['$httpProvider', function ($httpProvider) {
    
          $httpProvider.defaults.withCredentials = true;
          $httpProvider.interceptors.push('XSRFInterceptor');
    
        }]);
    
Sanjay
  • 8,755
  • 7
  • 46
  • 62