From f462134e87971276aa28f7a40a8fe2b26be81593 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 1 Sep 2022 16:10:55 -0500 Subject: [PATCH] Add reactive support for BREACH Closes gh-11959 --- .../web/server/ServerHttpSecurityTests.java | 35 ++++ .../ROOT/pages/reactive/exploits/csrf.adoc | 43 ++++- ...erverCsrfTokenRequestAttributeHandler.java | 116 ++++++++++++ .../web/server/csrf/CsrfWebFilterTests.java | 54 +++++- ...CsrfTokenRequestAttributeHandlerTests.java | 171 ++++++++++++++++++ 5 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java index 30bad495c0f..23660f14d9f 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java @@ -33,6 +33,8 @@ import reactor.test.publisher.TestPublisher; import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; @@ -69,6 +71,7 @@ import org.springframework.security.web.server.csrf.DefaultCsrfToken; import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository; import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler; +import org.springframework.security.web.server.csrf.XorServerCsrfTokenRequestAttributeHandler; import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; import org.springframework.test.util.ReflectionTestUtils; @@ -526,6 +529,38 @@ public void postWhenCustomRequestHandlerThenUsed() { verify(requestHandler).resolveCsrfTokenValue(any(ServerWebExchange.class), any()); } + @Test + public void postWhenServerXorCsrfTokenRequestAttributeHandlerThenOk() { + CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token"); + given(this.csrfTokenRepository.loadToken(any(ServerWebExchange.class))).willReturn(Mono.just(csrfToken)); + given(this.csrfTokenRepository.generateToken(any(ServerWebExchange.class))).willReturn(Mono.empty()); + ServerCsrfTokenRequestHandler requestHandler = new XorServerCsrfTokenRequestAttributeHandler(); + // @formatter:off + this.http.csrf((csrf) -> csrf + .csrfTokenRepository(this.csrfTokenRepository) + .csrfTokenRequestHandler(requestHandler) + ); + // @formatter:on + + // Generate masked CSRF token value + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build()); + requestHandler.handle(exchange, Mono.just(csrfToken)); + Mono csrfTokenAttribute = exchange.getAttribute(CsrfToken.class.getName()); + String actualTokenValue = csrfTokenAttribute.map(CsrfToken::getToken).block(); + assertThat(actualTokenValue).isNotEqualTo(csrfToken.getToken()); + + WebTestClient client = buildClient(); + // @formatter:off + client.post() + .uri("/") + .header(csrfToken.getHeaderName(), actualTokenValue) + .exchange() + .expectStatus().isOk(); + // @formatter:on + verify(this.csrfTokenRepository, times(2)).loadToken(any(ServerWebExchange.class)); + verify(this.csrfTokenRepository).generateToken(any(ServerWebExchange.class)); + } + @Test public void shouldConfigureRequestCacheForOAuth2LoginAuthenticationEntryPointAndSuccessHandler() { ServerRequestCache requestCache = spy(new WebSessionServerRequestCache()); diff --git a/docs/modules/ROOT/pages/reactive/exploits/csrf.adoc b/docs/modules/ROOT/pages/reactive/exploits/csrf.adoc index 6762324171a..cc43b90ef9b 100644 --- a/docs/modules/ROOT/pages/reactive/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/reactive/exploits/csrf.adoc @@ -106,13 +106,54 @@ fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain ----- ==== +[[webflux-csrf-configure-request-handler]] +==== Configure ServerCsrfTokenRequestHandler + +Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html[`CsrfWebFilter`] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html[`Mono`] as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken` with the help of a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/ServerCsrfTokenRequestHandler.html[`ServerCsrfTokenRequestHandler`]. +The default implementation is `ServerCsrfTokenRequestAttributeHandler`. + +An alternate implementation `XorServerCsrfTokenRequestAttributeHandler` is available to provide protection for BREACH (see https://github.com/spring-projects/spring-security/issues/4001[gh-4001]). + +You can configure `XorServerCsrfTokenRequestAttributeHandler` using the following Java configuration: + +.Configure BREACH protection +==== +.Java +[source,java,role="primary"] +----- +@Bean +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .csrf(csrf -> csrf + .csrfTokenRequestHandler(new XorServerCsrfTokenRequestAttributeHandler()) + ) + return http.build(); +} +----- + +.Kotlin +[source,kotlin,role="secondary"] +----- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + csrf { + csrfTokenRequestHandler = XorServerCsrfTokenRequestAttributeHandler() + } + } +} +----- +==== + [[webflux-csrf-include]] === Include the CSRF Token In order for the xref:features/exploits/csrf.adoc#csrf-protection-stp[synchronizer token pattern] to protect against CSRF attacks, we must include the actual CSRF token in the HTTP request. This must be included in a part of the request (i.e. form parameter, HTTP header, etc) that is not automatically included in the HTTP request by the browser. -Spring Security's https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html[CsrfWebFilter] exposes a https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html[Mono] as a `ServerWebExchange` attribute named `org.springframework.security.web.server.csrf.CsrfToken`. +<> that the `Mono` is exposed as a `ServerWebExchange` attribute. This means that any view technology can access the `Mono` to expose the expected token as either a <> or <>. [[webflux-csrf-include-subscribe]] diff --git a/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java b/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java new file mode 100644 index 00000000000..8e1c53ece17 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandler.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2022 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.csrf; + +import java.security.SecureRandom; +import java.util.Base64; + +import reactor.core.publisher.Mono; + +import org.springframework.security.crypto.codec.Utf8; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * An implementation of the {@link ServerCsrfTokenRequestAttributeHandler} and + * {@link ServerCsrfTokenRequestResolver} interfaces that is capable of masking the value + * of the {@link CsrfToken} on each request and resolving the raw token value from the + * masked value as either a form data value or header of the request. + * + * @author Steve Riesenberg + * @since 5.8 + */ +public final class XorServerCsrfTokenRequestAttributeHandler extends ServerCsrfTokenRequestAttributeHandler { + + private SecureRandom secureRandom = new SecureRandom(); + + /** + * Specifies the {@code SecureRandom} used to generate random bytes that are used to + * mask the value of the {@link CsrfToken} on each request. + * @param secureRandom the {@code SecureRandom} to use to generate random bytes + */ + public void setSecureRandom(SecureRandom secureRandom) { + Assert.notNull(secureRandom, "secureRandom cannot be null"); + this.secureRandom = secureRandom; + } + + @Override + public void handle(ServerWebExchange exchange, Mono csrfToken) { + Assert.notNull(exchange, "exchange cannot be null"); + Assert.notNull(csrfToken, "csrfToken cannot be null"); + Mono updatedCsrfToken = csrfToken.map((token) -> new DefaultCsrfToken(token.getHeaderName(), + token.getParameterName(), createXoredCsrfToken(this.secureRandom, token.getToken()))); + super.handle(exchange, updatedCsrfToken); + } + + @Override + public Mono resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) { + return super.resolveCsrfTokenValue(exchange, csrfToken) + .flatMap((actualToken) -> Mono.justOrEmpty(getTokenValue(actualToken, csrfToken.getToken()))); + } + + private static String getTokenValue(String actualToken, String token) { + byte[] actualBytes; + try { + actualBytes = Base64.getUrlDecoder().decode(actualToken); + } + catch (Exception ex) { + return null; + } + + byte[] tokenBytes = Utf8.encode(token); + int tokenSize = tokenBytes.length; + if (actualBytes.length < tokenSize) { + return null; + } + + // extract token and random bytes + int randomBytesSize = actualBytes.length - tokenSize; + byte[] xoredCsrf = new byte[tokenSize]; + byte[] randomBytes = new byte[randomBytesSize]; + + System.arraycopy(actualBytes, 0, randomBytes, 0, randomBytesSize); + System.arraycopy(actualBytes, randomBytesSize, xoredCsrf, 0, tokenSize); + + byte[] csrfBytes = xorCsrf(randomBytes, xoredCsrf); + return Utf8.decode(csrfBytes); + } + + private static String createXoredCsrfToken(SecureRandom secureRandom, String token) { + byte[] tokenBytes = Utf8.encode(token); + byte[] randomBytes = new byte[tokenBytes.length]; + secureRandom.nextBytes(randomBytes); + + byte[] xoredBytes = xorCsrf(randomBytes, tokenBytes); + byte[] combinedBytes = new byte[tokenBytes.length + randomBytes.length]; + System.arraycopy(randomBytes, 0, combinedBytes, 0, randomBytes.length); + System.arraycopy(xoredBytes, 0, combinedBytes, randomBytes.length, xoredBytes.length); + + return Base64.getUrlEncoder().encodeToString(combinedBytes); + } + + private static byte[] xorCsrf(byte[] randomBytes, byte[] csrfBytes) { + int len = Math.min(randomBytes.length, csrfBytes.length); + byte[] xoredCsrf = new byte[len]; + System.arraycopy(csrfBytes, 0, xoredCsrf, 0, csrfBytes.length); + for (int i = 0; i < len; i++) { + xoredCsrf[i] ^= randomBytes[i]; + } + return xoredCsrf; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java index a97fad9d0b4..c938829a0e2 100644 --- a/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/csrf/CsrfWebFilterTests.java @@ -180,6 +180,44 @@ public void filterWhenRequestHandlerSetThenUsed() { verify(requestHandler).resolveCsrfTokenValue(this.post, this.token); } + @Test + public void filterWhenXorServerCsrfTokenRequestProcessorAndValidTokenThenSuccess() { + PublisherProbe chainResult = PublisherProbe.empty(); + given(this.chain.filter(any())).willReturn(chainResult.mono()); + this.csrfFilter.setCsrfTokenRepository(this.repository); + given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); + given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); + XorServerCsrfTokenRequestAttributeHandler requestHandler = new XorServerCsrfTokenRequestAttributeHandler(); + this.csrfFilter.setRequestHandler(requestHandler); + StepVerifier.create(this.csrfFilter.filter(this.get, this.chain)).verifyComplete(); + chainResult.assertWasSubscribed(); + + Mono csrfTokenAttribute = this.get.getAttribute(CsrfToken.class.getName()); + assertThat(csrfTokenAttribute).isNotNull(); + StepVerifier.create(csrfTokenAttribute) + .consumeNextWith((csrfToken) -> this.post = MockServerWebExchange + .from(MockServerHttpRequest.post("/").header(csrfToken.getHeaderName(), csrfToken.getToken()))) + .verifyComplete(); + + StepVerifier.create(this.csrfFilter.filter(this.post, this.chain)).verifyComplete(); + chainResult.assertWasSubscribed(); + } + + @Test + public void filterWhenXorServerCsrfTokenRequestProcessorAndRawTokenThenAccessDeniedException() { + PublisherProbe chainResult = PublisherProbe.empty(); + this.csrfFilter.setCsrfTokenRepository(this.repository); + given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); + XorServerCsrfTokenRequestAttributeHandler requestHandler = new XorServerCsrfTokenRequestAttributeHandler(); + this.csrfFilter.setRequestHandler(requestHandler); + this.post = MockServerWebExchange + .from(MockServerHttpRequest.post("/").header(this.token.getHeaderName(), this.token.getToken())); + Mono result = this.csrfFilter.filter(this.post, this.chain); + StepVerifier.create(result).verifyComplete(); + chainResult.assertWasNotSubscribed(); + assertThat(this.post.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + @Test // gh-8452 public void matchesRequireCsrfProtectionWhenNonStandardHTTPMethodIsUsed() { @@ -215,7 +253,9 @@ public void filterWhenMultipartFormDataAndNotEnabledThenDenied() { @Test public void filterWhenMultipartFormDataAndEnabledThenGranted() { this.csrfFilter.setCsrfTokenRepository(this.repository); - this.csrfFilter.setTokenFromMultipartDataEnabled(true); + ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler(); + requestHandler.setTokenFromMultipartDataEnabled(true); + this.csrfFilter.setRequestHandler(requestHandler); given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); @@ -227,7 +267,9 @@ public void filterWhenMultipartFormDataAndEnabledThenGranted() { @Test public void filterWhenPostAndMultipartFormDataEnabledAndNoBodyProvided() { this.csrfFilter.setCsrfTokenRepository(this.repository); - this.csrfFilter.setTokenFromMultipartDataEnabled(true); + ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler(); + requestHandler.setTokenFromMultipartDataEnabled(true); + this.csrfFilter.setRequestHandler(requestHandler); given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); @@ -238,7 +280,9 @@ public void filterWhenPostAndMultipartFormDataEnabledAndNoBodyProvided() { @Test public void filterWhenFormDataAndEnabledThenGranted() { this.csrfFilter.setCsrfTokenRepository(this.repository); - this.csrfFilter.setTokenFromMultipartDataEnabled(true); + ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler(); + requestHandler.setTokenFromMultipartDataEnabled(true); + this.csrfFilter.setRequestHandler(requestHandler); given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); given(this.repository.generateToken(any())).willReturn(Mono.just(this.token)); WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); @@ -250,7 +294,9 @@ public void filterWhenFormDataAndEnabledThenGranted() { @Test public void filterWhenMultipartMixedAndEnabledThenNotRead() { this.csrfFilter.setCsrfTokenRepository(this.repository); - this.csrfFilter.setTokenFromMultipartDataEnabled(true); + ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler(); + requestHandler.setTokenFromMultipartDataEnabled(true); + this.csrfFilter.setRequestHandler(requestHandler); given(this.repository.loadToken(any())).willReturn(Mono.just(this.token)); WebTestClient client = WebTestClient.bindToController(new OkController()).webFilter(this.csrfFilter).build(); client.post().uri("/").contentType(MediaType.MULTIPART_MIXED) diff --git a/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java new file mode 100644 index 00000000000..43fdf2f3d77 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/csrf/XorServerCsrfTokenRequestAttributeHandlerTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2022 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.csrf; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link XorServerCsrfTokenRequestAttributeHandler}. + * + * @author Steve Riesenberg + * @since 5.8 + */ +public class XorServerCsrfTokenRequestAttributeHandlerTests { + + private static final byte[] XOR_CSRF_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 }; + + private static final String XOR_CSRF_TOKEN_VALUE = Base64.getEncoder().encodeToString(XOR_CSRF_TOKEN_BYTES); + + private XorServerCsrfTokenRequestAttributeHandler handler; + + private MockServerWebExchange exchange; + + private CsrfToken token; + + private SecureRandom secureRandom; + + @BeforeEach + public void setUp() { + this.handler = new XorServerCsrfTokenRequestAttributeHandler(); + this.exchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")).build(); + this.token = new DefaultCsrfToken("headerName", "paramName", "abc"); + this.secureRandom = mock(SecureRandom.class); + } + + @Test + public void setSecureRandomWhenNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setSecureRandom(null)) + .withMessage("secureRandom cannot be null"); + } + + @Test + public void handleWhenExchangeIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.handle(null, Mono.just(this.token))) + .withMessage("exchange cannot be null"); + } + + @Test + public void handleWhenCsrfTokenIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.handle(this.exchange, null)) + .withMessage("csrfToken cannot be null"); + } + + @Test + public void handleWhenSecureRandomSetThenUsed() { + this.handler.setSecureRandom(this.secureRandom); + this.handler.handle(this.exchange, Mono.just(this.token)); + Mono csrfTokenAttribute = this.exchange.getAttribute(CsrfToken.class.getName()); + assertThat(csrfTokenAttribute).isNotNull(); + StepVerifier.create(csrfTokenAttribute).expectNextCount(1).verifyComplete(); + verify(this.secureRandom).nextBytes(anyByteArray()); + } + + @Test + public void handleWhenValidParametersThenExchangeAttributeSet() { + willAnswer(fillByteArray()).given(this.secureRandom).nextBytes(anyByteArray()); + + this.handler.setSecureRandom(this.secureRandom); + this.handler.handle(this.exchange, Mono.just(this.token)); + Mono csrfTokenAttribute = this.exchange.getAttribute(CsrfToken.class.getName()); + assertThat(csrfTokenAttribute).isNotNull(); + // @formatter:off + StepVerifier.create(csrfTokenAttribute) + .assertNext((csrfToken) -> assertThat(csrfToken.getToken()).isEqualTo(XOR_CSRF_TOKEN_VALUE)) + .verifyComplete(); + // @formatter:on + verify(this.secureRandom).nextBytes(anyByteArray()); + } + + @Test + public void resolveCsrfTokenValueWhenExchangeIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.resolveCsrfTokenValue(null, this.token)) + .withMessage("exchange cannot be null"); + } + + @Test + public void resolveCsrfTokenValueWhenCsrfTokenIsNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.handler.resolveCsrfTokenValue(this.exchange, null)) + .withMessage("csrfToken cannot be null"); + } + + @Test + public void resolveCsrfTokenValueWhenTokenNotSetThenReturnsEmptyMono() { + Mono csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, this.token); + StepVerifier.create(csrfToken).verifyComplete(); + } + + @Test + public void resolveCsrfTokenValueWhenFormDataSetThenReturnsTokenValue() { + this.exchange = MockServerWebExchange.builder(MockServerHttpRequest.post("/") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .body(this.token.getParameterName() + "=" + XOR_CSRF_TOKEN_VALUE)).build(); + Mono csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, this.token); + StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete(); + } + + @Test + public void resolveCsrfTokenValueWhenHeaderSetThenReturnsTokenValue() { + this.exchange = MockServerWebExchange.builder(MockServerHttpRequest.post("/") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(this.token.getHeaderName(), XOR_CSRF_TOKEN_VALUE)).build(); + Mono csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, this.token); + StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete(); + } + + @Test + public void resolveCsrfTokenValueWhenHeaderAndFormDataSetThenFormDataIsPreferred() { + this.exchange = MockServerWebExchange.builder(MockServerHttpRequest.post("/") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .header(this.token.getHeaderName(), "header") + .body(this.token.getParameterName() + "=" + XOR_CSRF_TOKEN_VALUE)).build(); + Mono csrfToken = this.handler.resolveCsrfTokenValue(this.exchange, this.token); + StepVerifier.create(csrfToken).expectNext(this.token.getToken()).verifyComplete(); + } + + private static Answer fillByteArray() { + return (invocation) -> { + byte[] bytes = invocation.getArgument(0); + Arrays.fill(bytes, (byte) 1); + return null; + }; + } + + private static byte[] anyByteArray() { + return any(byte[].class); + } + +}