1

I have a small Rest-Service App (Java 8, Spring 4.1.6, Spring Security 5.0.1, Jetty 9.3) and i'am accessing some services by JSON using Spring RestTemplate. Until now csfr was disabled, now i want to enable it.

As is understood csfr there is a common token (the client sends it with each request, the server stores it in the session) which is compared on server side. Access is denied if there is no token available or the token is different.

So i thought it would be a good idea to do this token-adding by using an interceptor. I also read, that in json i have to send the token as a header-parameter... but i did something wrong, already the login fails.

Here is the login-source:

MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", username);
form.add("password", password);
return restTemplate.postForLocation(serverUri + "login", form);

Here the source of the interceptor:

import java.io.IOException;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class MyCsfrInterceptor implements ClientHttpRequestInterceptor{

    public static final String CSRF_TOKEN_HEADER_NAME = "X-CSRF-TOKEN";
    public static final String csrfSessionToken = UUID.randomUUID().toString();

    private static Logger LOG = LoggerFactory.getLogger(MyCsfrInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        LOG.info("My interceptor called!");
        if (request.getMethod() == HttpMethod.DELETE ||
                request.getMethod() == HttpMethod.POST ||
                request.getMethod() == HttpMethod.PATCH ||
                request.getMethod() == HttpMethod.PUT){
            LOG.info("Setting csrf token...");
            request.getHeaders().add(CSRF_TOKEN_HEADER_NAME, csrfSessionToken);
        }
        return execution.execute(request, body);
    }

}

Here is the log output on client side:

23:24:40.605 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - Created POST request for "http://localhost:8080/login"
23:24:40.610 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - Writing [{username=[user], password=[user]}] using [org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@618b19ad]
23:24:40.615 [main] INFO  c.m.l.w.client.MyCsfrInterceptor - My interceptor called!
23:24:40.615 [main] INFO  c.m.l.w.client.MyCsfrInterceptor - Setting csrf token...
23:24:40.650 [main] DEBUG o.a.h.c.protocol.RequestAddCookies - CookieSpec selected: best-match
23:24:40.670 [main] DEBUG o.a.h.c.protocol.RequestAuthCache - Auth cache not set in the context
23:24:40.675 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://localhost:8080][total kept alive: 0; route allocated: 0 of 2; total allocated: 0 of 20]
23:24:40.705 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection leased: [id: 0][route: {}->http://localhost:8080][total kept alive: 0; route allocated: 1 of 2; total allocated: 1 of 20]
23:24:40.710 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Opening connection {}->http://localhost:8080
23:24:40.715 [main] DEBUG o.a.h.i.c.HttpClientConnectionOperator - Connecting to localhost/127.0.0.1:8080
23:24:40.715 [main] DEBUG o.a.h.i.c.HttpClientConnectionOperator - Connection established 127.0.0.1:54712<->127.0.0.1:8080
23:24:40.715 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Executing request POST /login HTTP/1.1
23:24:40.715 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Target auth state: UNCHALLENGED
23:24:40.720 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Proxy auth state: UNCHALLENGED
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> POST /login HTTP/1.1
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Type: application/x-www-form-urlencoded
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> X-CSRF-TOKEN: 99ca8171-b8e0-4b95-8d06-3759a874c64b
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Content-Length: 27
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Host: localhost:8080
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Connection: Keep-Alive
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> User-Agent: Apache-HttpClient/4.3.4 (java 1.5)
23:24:40.720 [main] DEBUG org.apache.http.headers - http-outgoing-0 >> Accept-Encoding: gzip,deflate
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "POST /login HTTP/1.1[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Type: application/x-www-form-urlencoded[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "X-CSRF-TOKEN: 99ca8171-b8e0-4b95-8d06-3759a874c64b[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Content-Length: 27[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Host: localhost:8080[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.3.4 (java 1.5)[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
23:24:40.720 [main] DEBUG org.apache.http.wire - http-outgoing-0 >> "username=user&password=user"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 403 Expected CSRF token not found. Has your session expired?[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Date: Sat, 15 Aug 2015 21:24:40 GMT[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Pragma: no-cache[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-XSS-Protection: 1; mode=block[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-Frame-Options: DENY[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "X-Content-Type-Options: nosniff[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Set-Cookie: JSESSIONID=1bvmexep1lv9h1qja44hflx0wg;Path=/[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Type: text/html;charset=iso-8859-1[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Cache-Control: must-revalidate,no-cache,no-store[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Length: 409[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Server: Jetty(9.3.0.RC1)[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<html>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<head>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<title>Error 403 Expected CSRF token not found. Has your session expired?</title>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</head>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<body><h2>HTTP ERROR 403</h2>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<p>Problem accessing /login. Reason:[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "<pre>    Expected CSRF token not found. Has your session expired?</pre></p><hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.0.RC1</a><hr/>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</body>[\n]"
23:24:40.831 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "</html>[\n]"
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << HTTP/1.1 403 Expected CSRF token not found. Has your session expired?
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Date: Sat, 15 Aug 2015 21:24:40 GMT
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Pragma: no-cache
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-XSS-Protection: 1; mode=block
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-Frame-Options: DENY
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << X-Content-Type-Options: nosniff
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Set-Cookie: JSESSIONID=1bvmexep1lv9h1qja44hflx0wg;Path=/
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Type: text/html;charset=iso-8859-1
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Cache-Control: must-revalidate,no-cache,no-store
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Length: 409
23:24:40.836 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Server: Jetty(9.3.0.RC1)
23:24:40.846 [main] DEBUG o.a.h.impl.execchain.MainClientExec - Connection can be kept alive indefinitely
23:24:40.861 [main] DEBUG o.a.h.c.p.ResponseProcessCookies - Cookie accepted [JSESSIONID="1bvmexep1lv9h1qja44hflx0wg", version:0, domain:localhost, path:/, expiry:null]
23:24:40.861 [main] DEBUG c.m.l.w.client.StatefulRestTemplate - POST request for "http://localhost:8080/login" resulted in 403 (Expected CSRF token not found. Has your session expired?); invoking error handler
23:24:40.866 [main] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection [id: 0][route: {}->http://localhost:8080] can be kept alive indefinitely

In spring security config i didn't do any csrf configuration.

So what's wrong? Did i set the header wrong? Any config missing?

Best regards and thank you for your time.

user3227576
  • 554
  • 8
  • 22

1 Answers1

2

Looking at your code, it seems that you are generating the CSRF token yourself. But, as I understand, Spring Security CSRF handling would work this way:

  1. Spring Security would generate the CSRF token.
  2. Whenever a request comes (say a GET request) Spring Security will attach the token as a request parameter. This helps rendering JSP forms with the token as a hidden field, like this:

    <form action="/foo/5/update" method="post">
        <input type="text" ... />
        <input type="submit" value="Update" />
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    </form>
    
  3. Whenever a POST like request comes (say a form getting submitted), Spring Security will match its token with the one submitted as a parameter or header.

(2) above works well when we are using JSP etc. But, when we are calling an API, we will first need to fetch the token. A common practice to do that is to have a filter at the server side, attaching the token as a cookie. This is my filter code in a project:

public class CsrfCookieFilter extends OncePerRequestFilter {

    public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {      

        log.debug("Inside CsrfCookieFilter ...");

        CsrfToken csrf = (CsrfToken)
            request.getAttribute(CsrfToken.class.getName()); // Or "_csrf" (See CSRFFilter.doFilterInternal).

        if (csrf != null) {
            Cookie cookie = WebUtils.getCookie(
                request, XSRF_TOKEN_COOKIE_NAME);
            String token = csrf.getToken();
            if (cookie==null ||
                token!=null && !token.equals(cookie.getValue())) {
                cookie = new Cookie(XSRF_TOKEN_COOKIE_NAME, token);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }       
        filterChain.doFilter(request, response);
    }
}

So, before a POST request, a GET request first should come and fetch the token as a cookie. The token then should be sent back as a header in subsequent requests.

My code for wiring this filter and altering the header name looks like this inside inside my security configuration class:

@Override
protected void configure(HttpSecurity http) throws Exception {

    http
        ...
        .addFilterAfter(csrfCookieFilter(), CsrfFilter.class)
        ...
}

protected Filter csrfCookieFilter() {
    return new CsrfCookieFilter();
}

Also, note that Spting Security changes the token after certain events like login and logout. So, we need to again fetch the token with a GET request.

The official Spring Angular guide has elaborated it in details.

Community
  • 1
  • 1
Sanjay
  • 8,755
  • 7
  • 46
  • 62
  • Hi! First of all thank you for your answer. I've tried your solution, it partly worked (i think i do still something wrong). Before a login i do a dummy get request to receive a token. This works, the token is set, i read the cookie-token and add it to the request. The login works. But: The first request, after the login-call fails (Invalid CSRF token). Reason: The CsrfCookieFilter wasn't called after login to refresh the cookie token. I've added the CsrfCookieFilter in web.xml after DelegatingFilterProxy, but this seems to be wrong. Do i have to change the filterchan to get it work? – user3227576 Aug 16 '15 at 21:01
  • After certain events like login and logout, another GET request would be needed. For details, see the stack overflow question linked from the last but one para of my answer. – Sanjay Aug 16 '15 at 22:25
  • Ok, now it works. Thank you very much. Just one more question: Why the CsrfCookieFilter wasn't called after the login? Because if it would have called after the login, the new token already had been set... so the get-request after the login won't be necessary. Is the reason that a new session was generated after the login? – user3227576 Aug 17 '15 at 08:15
  • I did some try digging Spring Security code, but couldn't make out. The comments in my post that is linked from 2nd last para says what you suspect. But the new session id will anyway go back in the response. No idea. – Sanjay Aug 17 '15 at 09:58