Describe the bug

Angular compatibility is broken with Spring Boot 3 RC1 and implementation of the XOR token generation.

The cookie token provider configured as described in documentation provides a header CSRF token. This token still follows the previous UUID pattern. If this token is used in a post request, then the request results in HTTP status 403 response.

To Reproduce I implemented unit tests to demo my use case.

In the use case I have a REST controller providing a CSRF token as a JSON body. This way I get 2 different tokens from the same controller. The one in the response body and the one automatically appended in the header from filter.

Expected behavior My expectation is, the header cookie contains a valid CSRF token usable with the current XSRF mechanism existing in current Angular 14 or 15 version.

Comment From: wagnereliakim

Hello @rbreunung,

I don't know if it can solve your specific problem, but have you seen this in the documentation?

I've tested a REST request with JSON body (with Postman) sending the X-XSRF-TOKEN header and it worked perfectly. For application/x-www-form-urlencoded forms the parameter remains "_csrf".

Maybe this can also help you.

Comment From: sjohnr

Hi @rbreunung, thanks for reaching out!

As mentioned by @wagnereliakim, the section I am using AngularJS or another Javascript framework of the 5.8 migration guide outlines steps that Angular users like yourself can take to upgrade your application.

See also this answer for a bit more context on the issue. The summary is that the raw CSRF token (contained in the XSRF-TOKEN cookie) is no longer accepted by default, and a separate mechanism (such as a response header, JSON response, etc.) would need to be used to fetch a value that is acceptable for the X-XSRF-TOKEN request header. This change is intentional and not a bug, though it is unfortunate that it has an impact on Angular users.

As outlined in the migration guide, you can configure Spring Security to accept the raw CSRF token contained in the cookie if you desire. I'm going to close this issue as this is already covered in the reference documentation.

Comment From: rbreunung

Hey @sjohnr, @wagnereliakim,

thank you very much for your answers. I think you understand my issue. But: 1. I don't like the proposed solution, because it is no solution but a step back. 2. The documented workaround does not work for me.

I changed the code a bit to get it working:

        CsrfTokenRequestAttributeHandler delegate = new CsrfTokenRequestAttributeHandler();
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(delegate::handle);

If I got your workaround correctly, the idea is to disable the mechanism until a proper solution is available. In your linked example still the XorHandler remains in place and the header authentication is disabled.

Maybe you can give me your perspective on this?

Thanks Robert

Comment From: sjohnr

Hi @rbreunung!

I don't like the proposed solution, because it is no solution but a step back.

The listed solution keeps BREACH protection in place for defense in-depth, but allows the raw CSRF token to be submitted as before. I would recommend finding a reputable source for researching BREACH more thoroughly, as I don't think discussing it deeply here would be productive.

In your linked example still the XorHandler remains in place and the header authentication is disabled.

This was not the case in my testing while working on the migration guide linked above. The code you provided above is equivalent to this:

        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler());

The documented workaround does not work for me.

Could you provide a minimal sample or some additional details? The unit tests listed above are part of another project, so a minimal sample would be helpful for seeing what you're seeing.

Comment From: rbreunung

Hi @sjohnr ,

thank you for answering and taking care.

I copy the example from documentation you linked:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
    // set the name of the attribute the CsrfToken will be populated on
    delegate.setCsrfRequestAttributeName("_csrf");
    // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
    // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
    CsrfTokenRequestHandler requestHandler = delegate::handle;
    http
        // ...
        .csrf((csrf) -> csrf
            .csrfTokenRepository(tokenRepository)
            .csrfTokenRequestHandler(requestHandler)
        );

    return http.build();
}

The critical point is the request handler (delegate) remains the XorCsrfTokenRequestAttributeHandler delegate. The handler is the one with BREACH protection. This is also the one validating the request from Angular.

  • CookieCsrfTokenRepository tokenRepository <- this guy is providing the plain token in the cookie, where Angular takes it.
  • XorCsrfTokenRequestAttributeHandler delegate <- this guy expects the new style token at the POST header, where the Angular XSRF module puts it.

The project I'm linking to is no real project and pretty minimal down to the use case. Anyway, I try my best to boil down the relevant part.

@Data
public class CsrfDto {

    private String parameterName;
    private String token;
    private String headerName;
}

This is how a JSON deserializing object for a JSON serialized Spring CSRF token may look.

From Spring Security perspective, we start as an anonymous user. We want to get a CSRF token to make a valid login POST.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class LoginTest {

    @LocalServerPort
    int serverPort;

    // we use rest template to get a more realistic client behavior
    @Autowired
    private TestRestTemplate webclient;

    @Test
    void postAuthenticate_validRequestUsingHeaderCsrf_userDto() {

        // query the RestController to get a serialized CSRF token as body payload
        // this is the XORed token
        // Received CSRF token: CsrfDto(parameterName=_csrf, token=Nw1LPaSuHdYRM0MnUrR5TlFZm_kQqwRasYJvUSRkIoZLeqeZVWl_XpHKJOE8CicVZZlNeGRttpt2nzJ30uBcYxRUG7UoG5L6, headerName=X-XSRF-TOKEN)
        ResponseEntity<CsrfDto> entity = getCsrfResponse();
        // Spring Security token repository is adding a session cookie containing the token as well
        // Cookies received from CSRF controller: [XSRF-TOKEN=a426bbcb-bac8-4b38-83b9-566f72206a6a]
        List<HttpCookie> newCsrfCookies = getNewCookies(entity.getHeaders());

I hope you already get the idea at this point. From the response entity I get two different tokens. One in the body for debugging purpose is the XORed and the other relevant one in the cookie is not XORed. At this point Angular takes the cookie variant.

Now the XOR-handler handles the header validation on POST request from client.

        // create a request where everything is in place: csrfToken (not XORed (Angular)), cookie as session cookie, URI, valid login credentials
        RequestEntity<LinkedMultiValueMap<String, String>> request = createLoginRequest(csrfDto, newCsrfCookies,
                REST_PATH_AUTHENTICATE, getLoginFormBody());
        ResponseEntity<String> responseEntity = webclient.exchange(request, String.class);

        // we receive a 403 here because the provided token is not subject to the XOR operation
        assertEquals(OK, responseEntity.getStatusCode(), "Expect successful login.");

Angular takes a token from cookie and presents it in the header on demand. To get the two things in sync Spring can either: - provide a XORed cookie value on demand. - disable the approval of XORed values only as header POST property -> approve also non XORed tokens

I think the documentation tried to achieve the second one, but it seems not to work in my use case.

Comment From: sjohnr

Hi @rbreunung,

Apologies, I'm attempting to understand your points as best I can. I believe the Java code you provided is coming from the unit test(s) linked above, but it's quite hard to follow as you're only providing some of the relevant code. It seems the unit tests are attempting to simulate Angular's behavior instead of using Angular directly in a minimal sample to reproduce. When I tested, I was using Angular directly.

Looking at your example above, the relevant piece is actually in createLoginRequest(...), which is not included above. I followed the links in your opening comment to try and understand what it's doing. This is the relevant section:

        if (cookieList != null) {
            headers.addAll(COOKIE, cookieList.stream().map(HttpCookie::toString).toList());
        }
        if (csrfDto != null) {
            headers.set(csrfDto.getHeaderName(), csrfDto.getToken());
        }

In my understanding and testing, this is not what Angular's HttpClientXsrfModule does. Angular actually obtains the raw token from the cookie and directly presents it in the header value X-XSRF-TOKEN. In your unit test, you are presenting the XOR'd token obtained from a custom endpoint (not the cookie value). Because you seem to be doing something different from Angular, if you simply use the default request handler provided by Spring Security in 6.0 (XorCsrfTokenRequestAttributeHandler), I believe your test will pass. The default would be equivalent to this explicit configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
    requestHandler.setCsrfRequestAttributeName("_csrf");
    http
        // ...
        .csrf((csrf) -> csrf
            .csrfTokenRepository(tokenRepository)
            .csrfTokenRequestHandler(requestHandler)
        );

    return http.build();
}

However, as pointed out in the migration guide and the above comment, Angular requires the workaround due to how it's obtaining the token that it presents in a header.

Does that make sense? Hopefully I'm understanding your code samples and unit tests correctly.