Describe the bug

I'm trying to configure Back-Channel Logout on an OAuth2 BFF: a reactive Spring Cloud Gateway instance configured with oauth2Login and the TokenRelay= filter.

As this BFF is used with single-page applications, CSRF protection is cookie-based (and this works for the RP-Initiated Logout: the POST request to /logout is correctly protected in this case).

During Back-Channel Logout, the internal request to /logout fails with 403 FORBIDDEN due to CSRF authorization failure.

Note that the Back-Channel Logout is successful as soon as: - I disable CSRF protection, but this makes the all system vulnerable to CSRF attacks (all REST requests from frontends are going through this BFF, initially authorized with a session cookie) - switch to session store for the CSRF token (but that breaks all POST, PUT, PATCH & DELETE requests from single-page & mobile apps).

This makes Back-Channel Logout unusable in production with single-page & mobile apps.

To Reproduce

Enable cookie-based protection against CSRF and then initiate a Back-Channel Logout.

Expected behavior

The Back-Channel Logout should be successful, whatever store is used for the CSRF token.

Why should the CSRF protection be enforced in a flow where the user agent is not involved?

Sample

Pre-requisites: - JDK between 17 & 21 on the path - node LTS on the path - Docker Desktop up

git clone https://github.com/ch4mpy/quiz.git
cd quiz
sh ./build.sh

This builds and composes all the services in docker.

Frontend URI is logged at the end of the build script (it contain the building machine hostname): http://hostname/ui/

Frontend users in quiz realm are ch4mp, moderator, trainee, and trainer. All have secret as secret.

To trigger a Back-Channel Logout, visit the Keycloak user account in the quiz realm and click the logout button from the top right corner: http://hostname/auth/realms/quiz/account/

To debug the OAuth2 client, stop the quiz.bff Docker container and start the api/bff Spring Boot project in debug mode with your favorite IDE.

Switching the CSRF protection strategy is just a matter of editing the com.c4-soft.springaddons.oidc.client.csrf property in the BFF application.yml.

Keycloak admin account is admin/admin: http://hostname/auth/admin/master/console/#/quiz

Possible fix?

Maybe OidcBackChannelServerLogoutHandler::eachLogout could be changed to something like that?

    private Mono<ResponseEntity<Void>> eachLogout(WebFilterExchange exchange, OidcSessionInformation session) {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
        for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
            headers.add(credential.getKey(), credential.getValue());
        }
        final var withCsrf = this.csrfTokenRepository instanceof CookieServerCsrfTokenRepository ?
                this.csrfTokenRepository.generateToken(exchange.getExchange()).flatMap(csrfToken -> this.csrfTokenRepository.saveToken(exchange.getExchange(), csrfToken)) :
                Mono.empty();
        return withCsrf.thenReturn(exchange.getExchange().getRequest()).flatMap(request -> {
            String logout = computeLogoutEndpoint(request);
            return this.web.post().uri(logout).headers((h) -> h.putAll(headers)).retrieve().toBodilessEntity();
        });
    }

With this, I expect that a CSRF token is generated and added in a cookie, but only in the case where the CSRF token repo is cookie-based.

Maybe is it acceptable to generate and save a CSRF token whatever the token repo is?

Comment From: jzheaux

Related to https://github.com/spring-projects/spring-security/issues/13841

Comment From: ch4mpy

@jzheaux I'd like to have an idea of when I'll be able to use Back-Channel Logout with my Spring clients serving single-page and mobile applications (with cookie-based protection against CSRF).

Is there a backlog, roadmap, or whatever, scheduling gh-15227, gh-13841 and gh-14510?

Comment From: jzheaux

Why should the CSRF protection be enforced in a flow where the user agent is not involved?

The reason CSRF is required is because Spring Security's POST /logout requires it by default. Alternatively, #13841 suggests that an improvement may be for /logout/connect/back-channel/* to make a call to itself with the appropriate sessionId, rendering the CSRF check unnecessary.

Based on your feedback, I've scheduled that ticket for 6.4.x. Given that I believe that will address your issue here, I'll close this and we can continue our collaboration over there.