diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
index 15ce91c39c..4cfb7c8222 100644
--- a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
+++ b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
@@ -123,6 +123,12 @@ class OAuth2LoginSecurityConfig {
If used, the application's base URL, such as `https://app.example.org`, replaces it at request time.
====
+[NOTE]
+====
+By default, `OidcClientInitiatedServerLogoutSuccessHandler` redirects to the logout URL using a standard HTTP redirect with the `GET` method.
+To perform the logout using a `POST` request, set the redirect strategy to `ServerFormPostRedirectStrategy`, for example with `OidcClientInitiatedServerLogoutSuccessHandler.setRedirectStrategy(new ServerFormPostRedirectStrategy())`.
+====
+
[[configure-provider-initiated-oidc-logout]]
== OpenID Connect 1.0 Back-Channel Logout
diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java
new file mode 100644
index 0000000000..2836f9ca85
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.util.HtmlUtils;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Redirect using an auto-submitting HTML form using the POST method. All query params
+ * provided in the URL are changed to inputs in the form so they are submitted as POST
+ * data instead of query string data.
+ *
+ * @author Max Batischev
+ * @since 6.5
+ */
+public final class ServerFormPostRedirectStrategy implements ServerRedirectStrategy {
+
+ private static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
+
+ private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator(
+ Base64.getUrlEncoder().withoutPadding(), 96);
+
+ private static final String REDIRECT_PAGE_TEMPLATE = """
+
+
+
+
+
+
+
+ Redirect
+
+
+
+
+
+
+ """;
+
+ private static final String HIDDEN_INPUT_TEMPLATE = """
+
+ """;
+
+ @Override
+ public Mono sendRedirect(ServerWebExchange exchange, URI location) {
+ String nonce = DEFAULT_NONCE_GENERATOR.generateKey();
+ String policyDirective = "script-src 'nonce-%s'".formatted(nonce);
+
+ ServerHttpResponse response = exchange.getResponse();
+ response.setStatusCode(HttpStatus.OK);
+ response.getHeaders().setContentType(MediaType.TEXT_HTML);
+ response.getHeaders().add(CONTENT_SECURITY_POLICY_HEADER, policyDirective);
+ return response.writeWith(createBuffer(exchange, location, nonce));
+ }
+
+ private Mono createBuffer(ServerWebExchange exchange, URI location, String nonce) {
+ byte[] bytes = createPage(location, nonce);
+ DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
+ return Mono.just(bufferFactory.wrap(bytes));
+ }
+
+ private byte[] createPage(URI location, String nonce) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(location);
+
+ StringBuilder hiddenInputsHtmlBuilder = new StringBuilder();
+ for (final Map.Entry> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) {
+ final String name = entry.getKey();
+ for (final String value : entry.getValue()) {
+ // @formatter:off
+ final String hiddenInput = HIDDEN_INPUT_TEMPLATE
+ .replace("{{name}}", HtmlUtils.htmlEscape(name))
+ .replace("{{value}}", HtmlUtils.htmlEscape(value));
+ // @formatter:on
+ hiddenInputsHtmlBuilder.append(hiddenInput.trim());
+ }
+ }
+ // @formatter:off
+ return REDIRECT_PAGE_TEMPLATE
+ .replace("{{action}}", HtmlUtils.htmlEscape(uriComponentsBuilder.query(null).build().toUriString()))
+ .replace("{{params}}", hiddenInputsHtmlBuilder.toString())
+ .replace("{{nonce}}", HtmlUtils.htmlEscape(nonce))
+ .getBytes(StandardCharsets.UTF_8);
+ // @formatter:on
+ }
+
+}
diff --git a/web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java
new file mode 100644
index 0000000000..67d65d2ab0
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server;
+
+import java.net.URI;
+
+import org.assertj.core.api.ThrowingConsumer;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
+import org.springframework.mock.web.server.MockServerWebExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ServerFormPostRedirectStrategy}.
+ *
+ * @author Max Batischev
+ */
+public class ServerFormPostRedirectStrategyTests {
+
+ private static final String POLICY_DIRECTIVE_PATTERN = "script-src 'nonce-(.+)'";
+
+ private final ServerRedirectStrategy redirectStrategy = new ServerFormPostRedirectStrategy();
+
+ private final MockServerHttpRequest request = MockServerHttpRequest.get("https://localhost").build();
+
+ private final MockServerWebExchange webExchange = MockServerWebExchange.from(this.request);
+
+ @Test
+ public void redirectWhetLocationAbsoluteUriIsPresentThenRedirect() {
+ this.redirectStrategy.sendRedirect(this.webExchange, URI.create("https://example.com")).block();
+
+ MockServerHttpResponse response = this.webExchange.getResponse();
+ assertThat(response.getBodyAsString().block()).contains("action=\"https://example.com\"");
+ assertThat(this.webExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(this.webExchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
+ assertThat(this.webExchange.getResponse()).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void redirectWhetLocationRootRelativeUriIsPresentThenRedirect() {
+ this.redirectStrategy.sendRedirect(this.webExchange, URI.create("/test")).block();
+
+ MockServerHttpResponse response = this.webExchange.getResponse();
+ assertThat(response.getBodyAsString().block()).contains("action=\"/test\"");
+ assertThat(this.webExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(this.webExchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
+ assertThat(this.webExchange.getResponse()).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void redirectWhetLocationRelativeUriIsPresentThenRedirect() {
+ this.redirectStrategy.sendRedirect(this.webExchange, URI.create("test")).block();
+
+ MockServerHttpResponse response = this.webExchange.getResponse();
+ assertThat(response.getBodyAsString().block()).contains("action=\"test\"");
+ assertThat(this.webExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(this.webExchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
+ assertThat(this.webExchange.getResponse()).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void redirectWhenLocationAbsoluteUriWithFragmentIsPresentThenRedirect() {
+ this.redirectStrategy.sendRedirect(this.webExchange, URI.create("https://example.com/path#fragment")).block();
+
+ MockServerHttpResponse response = this.webExchange.getResponse();
+ assertThat(response.getBodyAsString().block()).contains("action=\"https://example.com/path#fragment\"");
+ assertThat(this.webExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(this.webExchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
+ assertThat(this.webExchange.getResponse()).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void redirectWhenLocationAbsoluteUilWithQueryParamsIsPresentThenRedirect() {
+ this.redirectStrategy
+ .sendRedirect(this.webExchange, URI.create("https://example.com/path?param1=one¶m2=two#fragment"))
+ .block();
+
+ MockServerHttpResponse response = this.webExchange.getResponse();
+ String content = response.getBodyAsString().block();
+ assertThat(this.webExchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(this.webExchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
+ assertThat(content).contains("action=\"https://example.com/path#fragment\"");
+ assertThat(content).contains("");
+ assertThat(content).contains("");
+ }
+
+ private ThrowingConsumer hasScriptSrcNonce() {
+ return (response) -> {
+ final String policyDirective = response.getHeaders().get("Content-Security-Policy").get(0);
+ assertThat(policyDirective).isNotEmpty();
+ assertThat(policyDirective).matches(POLICY_DIRECTIVE_PATTERN);
+
+ final String nonce = policyDirective.replaceFirst(POLICY_DIRECTIVE_PATTERN, "$1");
+ assertThat(response.getBodyAsString().block()).contains("