Skip to content

Back-Channel Logout fails with cookie-based CSRF protection #15227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ch4mpy opened this issue Jun 11, 2024 · 3 comments
Closed

Back-Channel Logout fails with cookie-based CSRF protection #15227

ch4mpy opened this issue Jun 11, 2024 · 3 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue type: bug A general bug

Comments

@ch4mpy
Copy link
Contributor

ch4mpy commented Jun 11, 2024

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?

@ch4mpy ch4mpy added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Jun 11, 2024
@jzheaux
Copy link
Contributor

jzheaux commented Jun 17, 2024

Related to #13841

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Jul 4, 2024

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

@jzheaux
Copy link
Contributor

jzheaux commented Jul 22, 2024

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.

@jzheaux jzheaux closed this as completed Jul 22, 2024
@jzheaux jzheaux added status: duplicate A duplicate of another issue in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Jul 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue type: bug A general bug
Projects
None yet
Development

No branches or pull requests

2 participants