diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
index f7cd752328..e26d77b866 100644
--- a/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
+++ b/docs/modules/ROOT/pages/servlet/oauth2/login/logout.adoc
@@ -125,7 +125,7 @@ If used, the application's base URL, such as `https://app.example.org`, replaces
[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 `FormRedirectStrategy`, for example with `OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormRedirectStrategy())`.
+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]]
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 0000000000..6445c078f6
--- /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/main/java/org/springframework/security/web/aot/hint/WebMvcSecurityRuntimeHints.java b/web/src/main/java/org/springframework/security/web/aot/hint/WebMvcSecurityRuntimeHints.java
index 54c4493064..86df3618f2 100644
--- a/web/src/main/java/org/springframework/security/web/aot/hint/WebMvcSecurityRuntimeHints.java
+++ b/web/src/main/java/org/springframework/security/web/aot/hint/WebMvcSecurityRuntimeHints.java
@@ -54,11 +54,6 @@ class WebMvcSecurityRuntimeHints implements RuntimeHintsRegistrar {
hints.resources().registerResource(webauthnJavascript);
}
- ClassPathResource redirect = new ClassPathResource("org/springframework/security/form-redirect.js");
- if (redirect.exists()) {
- hints.resources().registerResource(redirect);
- }
-
}
}
diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultResourcesFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultResourcesFilter.java
index 2b92717502..c2c80f19bd 100644
--- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultResourcesFilter.java
+++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultResourcesFilter.java
@@ -111,20 +111,4 @@ public final class DefaultResourcesFilter extends GenericFilterBean {
new MediaType("text", "javascript", StandardCharsets.UTF_8));
}
- /**
- * Create an instance of {@link DefaultResourcesFilter} serving Spring Security's
- * default webauthn javascript.
- *
- * The created {@link DefaultResourcesFilter} matches requests
- * {@code HTTP GET /form-redirect.js}, and returns the default webauthn javascript at
- * {@code org/springframework/security/form-redirect.js} with content-type
- * {@code text/javascript;charset=UTF-8}.
- * @return -
- */
- public static DefaultResourcesFilter formRedirectJavascript() {
- return new DefaultResourcesFilter(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/form-redirect.js"),
- new ClassPathResource("org/springframework/security/form-redirect.js"),
- new MediaType("text", "javascript", StandardCharsets.UTF_8));
- }
-
}
diff --git a/web/src/main/java/org/springframework/security/web/server/ui/DefaultResourcesWebFilter.java b/web/src/main/java/org/springframework/security/web/server/ui/DefaultResourcesWebFilter.java
index a147e1b301..7f96e6f0d9 100644
--- a/web/src/main/java/org/springframework/security/web/server/ui/DefaultResourcesWebFilter.java
+++ b/web/src/main/java/org/springframework/security/web/server/ui/DefaultResourcesWebFilter.java
@@ -98,21 +98,4 @@ public final class DefaultResourcesWebFilter implements WebFilter {
new MediaType("text", "css", StandardCharsets.UTF_8));
}
- /**
- * Create an instance of {@link DefaultResourcesWebFilter} serving Spring Security's
- * form redirect javascript.
- *
- * The created {@link DefaultResourcesFilter} matches requests
- * {@code HTTP GET /form-redirect.js}, and returns the default javascript at
- * {@code org/springframework/security/form-redirect.js} with content-type
- * {@code text/javascript;charset=UTF-8}.
- * @return -
- */
- public static DefaultResourcesWebFilter formRedirectJavascript() {
- return new DefaultResourcesWebFilter(
- new PathPatternParserServerWebExchangeMatcher("/form-redirect.js", HttpMethod.GET),
- new ClassPathResource("org/springframework/security/form-redirect.js"),
- new MediaType("text", "javascript", StandardCharsets.UTF_8));
- }
-
}
diff --git a/web/src/main/java/org/springframework/security/web/server/ui/FormRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/server/ui/FormRedirectStrategy.java
deleted file mode 100644
index e2b1f89fe4..0000000000
--- a/web/src/main/java/org/springframework/security/web/server/ui/FormRedirectStrategy.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 2002-2023 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.ui;
-
-import java.io.IOException;
-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.web.RedirectStrategy;
-import org.springframework.web.util.UriComponentsBuilder;
-
-/**
- * Redirect using an autosubmitting 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.
- */
-/* default */ class FormRedirectStrategy implements RedirectStrategy {
-
- private static final String REDIRECT_PAGE_TEMPLATE = """
-
-
-
-
-
-
-
- Redirect
-
-
-
-
-
-
-
-
-
- """;
-
- private static final String HIDDEN_INPUT_TEMPLATE = """
-
- """;
-
- @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();
- // inputs
- for (final Entry> entry : uriComponentsBuilder.build().getQueryParams().entrySet()) {
- final String name = entry.getKey();
- for (final String value : entry.getValue()) {
- hiddenInputsHtmlBuilder.append(HtmlTemplates.fromTemplate(HIDDEN_INPUT_TEMPLATE)
- .withValue("name", name)
- .withValue("value", value)
- .render());
- }
- }
-
- final String html = HtmlTemplates.fromTemplate(REDIRECT_PAGE_TEMPLATE)
- // clear the query string as we don't want that to be part of the form action
- // URL
- .withValue("action", uriComponentsBuilder.query(null).build().toUriString())
- .withRawHtml("params", hiddenInputsHtmlBuilder.toString())
- .withValue("contextPath", request.getContextPath())
- .render();
- response.setStatus(HttpStatus.OK.value());
- response.setContentType(MediaType.TEXT_HTML_VALUE);
- response.getWriter().write(html);
- response.getWriter().flush();
- }
-
-}
diff --git a/web/src/main/resources/org/springframework/security/form-redirect.js b/web/src/main/resources/org/springframework/security/form-redirect.js
deleted file mode 100644
index aecae36ee0..0000000000
--- a/web/src/main/resources/org/springframework/security/form-redirect.js
+++ /dev/null
@@ -1 +0,0 @@
-document.getElementById("redirectForm").submit();
diff --git a/web/src/test/java/org/springframework/security/web/server/ui/FormRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java
similarity index 68%
rename from web/src/test/java/org/springframework/security/web/server/ui/FormRedirectStrategyTests.java
rename to web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java
index 4eb7b4c3b6..a3be15cb77 100644
--- a/web/src/test/java/org/springframework/security/web/server/ui/FormRedirectStrategyTests.java
+++ b/web/src/test/java/org/springframework/security/web/FormPostRedirectStrategyTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * 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.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package org.springframework.security.web.server.ui;
+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;
@@ -30,9 +31,11 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.assertj.core.api.Assertions.assertThat;
-public class FormRedirectStrategyTests {
+public class FormPostRedirectStrategyTests {
- private FormRedirectStrategy formRedirectStrategy;
+ private static final String POLICY_DIRECTIVE_PATTERN = "script-src 'nonce-(.+)'";
+
+ private FormPostRedirectStrategy redirectStrategy;
private MockHttpServletRequest request;
@@ -40,7 +43,7 @@ public class FormRedirectStrategyTests {
@BeforeEach
public void beforeEach() {
- this.formRedirectStrategy = new FormRedirectStrategy();
+ this.redirectStrategy = new FormPostRedirectStrategy();
final MockServletContext mockServletContext = new MockServletContext();
mockServletContext.setContextPath("/contextPath");
// the request URL doesn't matter
@@ -50,39 +53,43 @@ public class FormRedirectStrategyTests {
@Test
public void absoluteUrlNoParametersRedirect() throws IOException {
- this.formRedirectStrategy.sendRedirect(this.request, this.response, "https://example.com");
+ 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.formRedirectStrategy.sendRedirect(this.request, this.response, "/test");
+ 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.formRedirectStrategy.sendRedirect(this.request, this.response, "test");
+ 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.formRedirectStrategy.sendRedirect(this.request, this.response, "https://example.com/path#fragment");
+ 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.formRedirectStrategy.sendRedirect(this.request, this.response,
+ 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);
@@ -91,6 +98,18 @@ public class FormRedirectStrategyTests {
.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("