Skip to content

Commit

Permalink
Polish gh-16551
Browse files Browse the repository at this point in the history
  • Loading branch information
sjohnr committed Feb 19, 2025
1 parent f35d956 commit d0c7a9b
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 37 deletions.
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/reactive/oauth2/login/logout.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public void setRedirectStrategyWhenGivenNullThenThrowsException() {
}

@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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

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;
Expand All @@ -41,15 +42,13 @@
* 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 = """
<!DOCTYPE html>
<html lang="en">
Expand Down Expand Up @@ -79,46 +78,46 @@ public final class ServerFormPostRedirectStrategy implements ServerRedirectStrat
<input name="{{name}}" type="hidden" value="{{value}}" />
""";

private static final StringKeyGenerator DEFAULT_NONCE_GENERATOR = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 96);

@Override
public Mono<Void> 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));
}
final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(location);

private Mono<DataBuffer> 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<String, List<String>> 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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
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();

Expand Down Expand Up @@ -89,7 +89,7 @@ public void redirectWhenLocationAbsoluteUriWithFragmentIsPresentThenRedirect() {
}

@Test
public void redirectWhenLocationAbsoluteUilWithQueryParamsIsPresentThenRedirect() {
public void redirectWhenLocationAbsoluteUriWithQueryParamsIsPresentThenRedirect() {
this.redirectStrategy
.sendRedirect(this.webExchange, URI.create("https://example.com/path?param1=one&param2=two#fragment"))
.block();
Expand All @@ -105,7 +105,7 @@ public void redirectWhenLocationAbsoluteUilWithQueryParamsIsPresentThenRedirect(

private ThrowingConsumer<MockServerHttpResponse> 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);

Expand Down

0 comments on commit d0c7a9b

Please sign in to comment.