diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
index 9edd4492bdb..e26d77b8661 100644
--- a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
+++ b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
@@ -122,6 +122,12 @@ class OAuth2LoginSecurityConfig {
If used, the application's base URL, such as `https://app.example.org`, replaces it at request time.
====
+[NOTE]
+====
+By default, `OidcClientInitiatedLogoutSuccessHandler` 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 `FormPostRedirectStrategy`, for example with `OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormPostRedirectStrategy())`.
+====
+
[[configure-provider-initiated-oidc-logout]]
== OpenID Connect 1.0 Back-Channel Logout
diff --git a/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java
new file mode 100644
index 00000000000..6445c078f65
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/FormPostRedirectStrategy.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map.Entry;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+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 Craig Andrews
+ * @author Steve Riesenberg
+ * @since 6.5
+ */
+public final class FormPostRedirectStrategy implements RedirectStrategy {
+
+ private static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
+
+ private static final String REDIRECT_PAGE_TEMPLATE = """
+
+
+
+
+
+
+
+ Redirect
+
+
+
+
+
+
+ """;
+
+ private static final String HIDDEN_INPUT_TEMPLATE = """
+
+ """;
+
+ private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator(
+ Base64.getUrlEncoder().withoutPadding(), 96);
+
+ @Override
+ public void sendRedirect(final HttpServletRequest request, final HttpServletResponse response, final String url)
+ throws IOException {
+ final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(url);
+
+ final StringBuilder hiddenInputsHtmlBuilder = new StringBuilder();
+ for (final 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());
+ }
+ }
+
+ // Create the script-src policy directive for the Content-Security-Policy header
+ final String nonce = DEFAULT_NONCE_GENERATOR.generateKey();
+ final String policyDirective = "script-src 'nonce-%s'".formatted(nonce);
+
+ // @formatter:off
+ final String html = REDIRECT_PAGE_TEMPLATE
+ // Clear the query string as we don't want that to be part of the form action URL
+ .replace("{{action}}", HtmlUtils.htmlEscape(uriComponentsBuilder.query(null).build().toUriString()))
+ .replace("{{params}}", hiddenInputsHtmlBuilder.toString())
+ .replace("{{nonce}}", HtmlUtils.htmlEscape(nonce));
+ // @formatter:on
+
+ response.setStatus(HttpStatus.OK.value());
+ response.setContentType(MediaType.TEXT_HTML_VALUE);
+ response.setHeader(CONTENT_SECURITY_POLICY_HEADER, policyDirective);
+ response.getWriter().write(html);
+ response.getWriter().flush();
+ }
+
+}
diff --git a/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java
new file mode 100644
index 00000000000..a3be15cb77a
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import org.assertj.core.api.ThrowingConsumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FormPostRedirectStrategyTests {
+
+ private static final String POLICY_DIRECTIVE_PATTERN = "script-src 'nonce-(.+)'";
+
+ private FormPostRedirectStrategy redirectStrategy;
+
+ private MockHttpServletRequest request;
+
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ public void beforeEach() {
+ this.redirectStrategy = new FormPostRedirectStrategy();
+ final MockServletContext mockServletContext = new MockServletContext();
+ mockServletContext.setContextPath("/contextPath");
+ // the request URL doesn't matter
+ this.request = MockMvcRequestBuilders.get("https://localhost").buildRequest(mockServletContext);
+ this.response = new MockHttpServletResponse();
+ }
+
+ @Test
+ public void absoluteUrlNoParametersRedirect() throws IOException {
+ this.redirectStrategy.sendRedirect(this.request, this.response, "https://example.com");
+ assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
+ assertThat(this.response.getContentAsString()).contains("action=\"https://example.com\"");
+ assertThat(this.response).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void rootRelativeUrlNoParametersRedirect() throws IOException {
+ this.redirectStrategy.sendRedirect(this.request, this.response, "/test");
+ assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
+ assertThat(this.response.getContentAsString()).contains("action=\"/test\"");
+ assertThat(this.response).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void relativeUrlNoParametersRedirect() throws IOException {
+ this.redirectStrategy.sendRedirect(this.request, this.response, "test");
+ assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
+ assertThat(this.response.getContentAsString()).contains("action=\"test\"");
+ assertThat(this.response).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void absoluteUrlWithFragmentRedirect() throws IOException {
+ this.redirectStrategy.sendRedirect(this.request, this.response, "https://example.com/path#fragment");
+ assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
+ assertThat(this.response.getContentAsString()).contains("action=\"https://example.com/path#fragment\"");
+ assertThat(this.response).satisfies(hasScriptSrcNonce());
+ }
+
+ @Test
+ public void absoluteUrlWithQueryParamsRedirect() throws IOException {
+ this.redirectStrategy.sendRedirect(this.request, this.response,
+ "https://example.com/path?param1=one¶m2=two#fragment");
+ assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
+ assertThat(this.response.getContentAsString()).contains("action=\"https://example.com/path#fragment\"");
+ assertThat(this.response.getContentAsString())
+ .contains("");
+ assertThat(this.response.getContentAsString())
+ .contains("");
+ assertThat(this.response).satisfies(hasScriptSrcNonce());
+ }
+
+ private ThrowingConsumer hasScriptSrcNonce() {
+ return (response) -> {
+ final String policyDirective = response.getHeader("Content-Security-Policy");
+ assertThat(policyDirective).isNotEmpty();
+ assertThat(policyDirective).matches(POLICY_DIRECTIVE_PATTERN);
+
+ final String nonce = policyDirective.replaceFirst(POLICY_DIRECTIVE_PATTERN, "$1");
+ assertThat(response.getContentAsString()).contains("