diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc index 4cfb7c8222..af11f484d5 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc @@ -126,7 +126,7 @@ If used, the application's base URL, such as `https://app.example.org`, replaces [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())`. +To perform the logout using a `POST` request, set the redirect strategy to `FormPostServerRedirectStrategy`, for example with `OidcClientInitiatedServerLogoutSuccessHandler.setRedirectStrategy(new ServerFormPostRedirectStrategy())`. ==== [[configure-provider-initiated-oidc-logout]] diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java index 65c9bcdd51..682ee3819a 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java @@ -229,7 +229,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandlerTests { } @Test - public void logoutWhenCustomRedirectStrategySetThenCustomRedirectStrategyUse() { + public void logoutWhenCustomRedirectStrategySetThenCustomRedirectStrategyUsed() { ServerRedirectStrategy redirectStrategy = mock(ServerRedirectStrategy.class); given(redirectStrategy.sendRedirect(any(), any())).willReturn(Mono.empty()); OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(), diff --git a/web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java b/web/src/main/java/org/springframework/security/web/server/FormPostServerRedirectStrategy.java similarity index 71% rename from web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java rename to web/src/main/java/org/springframework/security/web/server/FormPostServerRedirectStrategy.java index 2836f9ca85..b6f2711ea0 100644 --- a/web/src/main/java/org/springframework/security/web/server/ServerFormPostRedirectStrategy.java +++ b/web/src/main/java/org/springframework/security/web/server/FormPostServerRedirectStrategy.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -41,15 +42,13 @@ import org.springframework.web.util.UriComponentsBuilder; * data instead of query string data. * * @author Max Batischev + * @author Steve Riesenberg * @since 6.5 */ -public final class ServerFormPostRedirectStrategy implements ServerRedirectStrategy { +public final class FormPostServerRedirectStrategy 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 = """ @@ -79,46 +78,46 @@ public final class ServerFormPostRedirectStrategy implements ServerRedirectStrat """; + private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator( + Base64.getUrlEncoder().withoutPadding(), 96); + @Override public Mono sendRedirect(ServerWebExchange exchange, URI location) { - String nonce = DEFAULT_NONCE_GENERATOR.generateKey(); - String policyDirective = "script-src 'nonce-%s'".formatted(nonce); + final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(location); - 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(); + final 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)); + .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 - 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); + 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 + + final ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.TEXT_HTML); + response.getHeaders().set(CONTENT_SECURITY_POLICY_HEADER, policyDirective); + + final DataBufferFactory bufferFactory = response.bufferFactory(); + final DataBuffer buffer = bufferFactory.wrap(html.getBytes(StandardCharsets.UTF_8)); + return response.writeWith(Mono.just(buffer)).doOnError((error) -> DataBufferUtils.release(buffer)); } } diff --git a/web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java b/web/src/test/java/org/springframework/security/web/server/FormPostServerRedirectStrategyTests.java similarity index 93% rename from web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java rename to web/src/test/java/org/springframework/security/web/server/FormPostServerRedirectStrategyTests.java index 67d65d2ab0..1a2124bb98 100644 --- a/web/src/test/java/org/springframework/security/web/server/ServerFormPostRedirectStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/server/FormPostServerRedirectStrategyTests.java @@ -30,15 +30,15 @@ import org.springframework.mock.web.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ServerFormPostRedirectStrategy}. + * Tests for {@link FormPostServerRedirectStrategy}. * * @author Max Batischev */ -public class ServerFormPostRedirectStrategyTests { +public class FormPostServerRedirectStrategyTests { private static final String POLICY_DIRECTIVE_PATTERN = "script-src 'nonce-(.+)'"; - private final ServerRedirectStrategy redirectStrategy = new ServerFormPostRedirectStrategy(); + private final ServerRedirectStrategy redirectStrategy = new FormPostServerRedirectStrategy(); private final MockServerHttpRequest request = MockServerHttpRequest.get("https://localhost").build(); @@ -89,7 +89,7 @@ public class ServerFormPostRedirectStrategyTests { } @Test - public void redirectWhenLocationAbsoluteUilWithQueryParamsIsPresentThenRedirect() { + public void redirectWhenLocationAbsoluteUriWithQueryParamsIsPresentThenRedirect() { this.redirectStrategy .sendRedirect(this.webExchange, URI.create("https://example.com/path?param1=one¶m2=two#fragment")) .block(); @@ -105,7 +105,7 @@ public class ServerFormPostRedirectStrategyTests { private ThrowingConsumer hasScriptSrcNonce() { return (response) -> { - final String policyDirective = response.getHeaders().get("Content-Security-Policy").get(0); + final String policyDirective = response.getHeaders().getFirst("Content-Security-Policy"); assertThat(policyDirective).isNotEmpty(); assertThat(policyDirective).matches(POLICY_DIRECTIVE_PATTERN);