diff --git a/web/src/main/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.java b/web/src/main/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.java new file mode 100644 index 0000000000..a962a127ab --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.reactive.result.view; + +import org.springframework.lang.NonNull; +import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.web.reactive.result.view.RequestDataValueProcessor; +import org.springframework.web.server.ServerWebExchange; + +import java.util.Collections; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class CsrfRequestDataValueProcessor implements RequestDataValueProcessor { + + private static final Pattern DISABLE_CSRF_TOKEN_PATTERN = Pattern + .compile("(?i)^(GET|HEAD|TRACE|OPTIONS)$"); + + private static final String DISABLE_CSRF_TOKEN_ATTR = "DISABLE_CSRF_TOKEN_ATTR"; + + @Override + public String processAction(ServerWebExchange exchange, String action, + String httpMethod) { + if (httpMethod != null && DISABLE_CSRF_TOKEN_PATTERN.matcher(httpMethod).matches()) { + exchange.getAttributes().put(DISABLE_CSRF_TOKEN_ATTR, Boolean.TRUE); + } + else { + exchange.getAttributes().remove(DISABLE_CSRF_TOKEN_ATTR); + } + return action; + } + + @Override + public String processFormFieldValue(ServerWebExchange exchange, + String name, String value, String type) { + return value; + } + + @NonNull + @Override + public Map getExtraHiddenFields( + ServerWebExchange exchange) { + if (Boolean.TRUE.equals(exchange.getAttribute(DISABLE_CSRF_TOKEN_ATTR))) { + exchange.getAttributes().remove(DISABLE_CSRF_TOKEN_ATTR); + return Collections.emptyMap(); + } + CsrfToken token = exchange.getAttribute(CsrfToken.class.getName()); + if(token == null) { + return Collections.emptyMap(); + } + return Collections.singletonMap(token.getParameterName(), token.getToken()); + } + + @Override + public String processUrl(ServerWebExchange exchange, String url) { + return url; + } +} diff --git a/web/src/test/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessorTests.java b/web/src/test/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessorTests.java new file mode 100644 index 0000000000..0091370c09 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessorTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.reactive.result.view; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.web.server.csrf.CsrfToken; +import org.springframework.security.web.server.csrf.DefaultCsrfToken; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class CsrfRequestDataValueProcessorTests { + private MockServerWebExchange exchange = exchange(HttpMethod.GET); + + private CsrfRequestDataValueProcessor processor = new CsrfRequestDataValueProcessor(); + + private CsrfToken token = new DefaultCsrfToken("1", "a", "b"); + private Map expected = new HashMap(); + + @Before + public void setup() { + this.expected.put(this.token.getParameterName(), this.token.getToken()); + this.exchange.getAttributes().put(CsrfToken.class.getName(), this.token); + } + + @Test + public void assertAllMethodsDeclared() { + Method[] expectedMethods = ReflectionUtils + .getAllDeclaredMethods(CsrfRequestDataValueProcessor.class); + for (Method expected : expectedMethods) { + assertThat( + ReflectionUtils.findMethod( + CsrfRequestDataValueProcessor.class, + expected.getName(), expected.getParameterTypes())).as( + "Expected to find " + expected + " defined on " + + CsrfRequestDataValueProcessor.class).isNotNull(); + } + } + + @Test + public void getExtraHiddenFieldsNoCsrfToken() { + this.exchange.getAttributes().clear(); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEmpty(); + } + + @Test + public void getExtraHiddenFieldsHasCsrfTokenNoMethodSet() { + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEqualTo(this.expected); + } + + @Test + public void getExtraHiddenFieldsHasCsrfToken_GET() { + this.processor.processAction(this.exchange, "action", "GET"); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEmpty(); + } + + @Test + public void getExtraHiddenFieldsHasCsrfToken_get() { + this.processor.processAction(this.exchange, "action", "get"); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEmpty(); + } + + @Test + public void getExtraHiddenFieldsHasCsrfToken_POST() { + this.processor.processAction(this.exchange, "action", "POST"); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEqualTo( + this.expected); + } + + @Test + public void getExtraHiddenFieldsHasCsrfToken_post() { + this.processor.processAction(this.exchange, "action", "post"); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEqualTo( + this.expected); + } + + @Test + public void processActionWithMethodArg() { + String action = "action"; + assertThat(this.processor.processAction(this.exchange, action, null)).isEqualTo(action); + } + + @Test + public void processFormFieldValue() { + String value = "action"; + assertThat(this.processor.processFormFieldValue(this.exchange, "name", value, "hidden")) + .isEqualTo(value); + } + + @Test + public void processUrl() { + String url = "url"; + assertThat(this.processor.processUrl(this.exchange, url)).isEqualTo(url); + } + + @Test + public void createGetExtraHiddenFieldsHasCsrfToken() { + CsrfToken token = new DefaultCsrfToken("1", "a", "b"); + this.exchange.getAttributes().put(CsrfToken.class.getName(), token); + Map expected = new HashMap(); + expected.put(token.getParameterName(), token.getToken()); + + CsrfRequestDataValueProcessor processor = new CsrfRequestDataValueProcessor(); + assertThat(this.processor.getExtraHiddenFields(this.exchange)).isEqualTo(expected); + } + + private MockServerWebExchange exchange(HttpMethod method) { + return MockServerWebExchange.from(MockServerHttpRequest.method(HttpMethod.GET, "/")); + } +}