Description
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?