diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 97690e577c..c0dcbaf2f8 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -13,8 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.config.web.server; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; @@ -40,11 +55,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; -import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter; import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter; import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter; +import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; @@ -84,6 +99,7 @@ import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository; import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter; import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter; import org.springframework.security.web.server.header.ServerHttpHeadersWriter; import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter; @@ -109,23 +125,9 @@ import org.springframework.web.cors.reactive.DefaultCorsProcessor; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Mono; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry; - /** * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux. * It allows configuring web based security for specific http requests. By default it will be applied @@ -178,6 +180,7 @@ import static org.springframework.security.web.server.DelegatingServerAuthentica * } * * @author Rob Winch + * @author Vedran Pavic * @since 5.0 */ public class ServerHttpSecurity { @@ -1659,6 +1662,8 @@ public class ServerHttpSecurity { private XXssProtectionServerHttpHeadersWriter xss = new XXssProtectionServerHttpHeadersWriter(); + private FeaturePolicyServerHttpHeadersWriter featurePolicy = new FeaturePolicyServerHttpHeadersWriter(); + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -1722,6 +1727,15 @@ public class ServerHttpSecurity { return new XssProtectionSpec(); } + /** + * Configures {@code Feature-Policy} response header. + * @param policyDirectives the policy directive(s) + * @return the {@link FeaturePolicySpec} to configure + */ + public FeaturePolicySpec featurePolicy(String policyDirectives) { + return new FeaturePolicySpec(policyDirectives); + } + /** * Configures cache control headers * @see #cache() @@ -1854,11 +1868,35 @@ public class ServerHttpSecurity { private XssProtectionSpec() {} } + /** + * Configures {@code Feature-Policy} response header. + * + * @see #featurePolicy(String) + * @since 5.1 + */ + public class FeaturePolicySpec { + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + private FeaturePolicySpec(String policyDirectives) { + HeaderSpec.this.featurePolicy.setPolicyDirectives(policyDirectives); + } + + } + private HeaderSpec() { this.writers = new ArrayList<>( - Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts, - this.frameOptions, this.xss)); + Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts, + this.frameOptions, this.xss, this.featurePolicy)); } + } /** diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index e1b0dfe658..7b37e5611f 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -16,36 +16,41 @@ package org.springframework.security.config.web.server; -import org.junit.Before; -import org.junit.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; -import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; -import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter; -import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter; -import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter; -import org.springframework.test.web.reactive.server.FluxExchangeResult; -import org.springframework.test.web.reactive.server.WebTestClient; - import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter; +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; + import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; /** + * Tests for {@link ServerHttpSecurity.HeaderSpec}. + * * @author Rob Winch + * @author Vedran Pavic * @since 5.0 */ public class HeaderSpecTests { - ServerHttpSecurity.HeaderSpec headers = ServerHttpSecurity.http().headers(); + private ServerHttpSecurity.HeaderSpec headers = ServerHttpSecurity.http().headers(); - HttpHeaders expectedHeaders = new HttpHeaders(); + private HttpHeaders expectedHeaders = new HttpHeaders(); - Set headerNamesNotPresent = new HashSet<>(); + private Set headerNamesNotPresent = new HashSet<>(); @Before public void setup() { @@ -143,6 +148,16 @@ public class HeaderSpecTests { assertHeaders(); } + @Test + public void headersWhenFeaturePolicyEnabledThenFeaturePolicyWritten() { + String policyDirectives = "Feature-Policy"; + this.expectedHeaders.add(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY, + policyDirectives); + this.headers.featurePolicy(policyDirectives); + + assertHeaders(); + } + private void expectHeaderNamesNotPresent(String... headerNames) { for(String headerName : headerNames) { this.expectedHeaders.remove(headerName); diff --git a/web/src/main/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriter.java new file mode 100644 index 0000000000..7f5704f10f --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 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.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Writes the {@code Feature-Policy} response header with configured policy directives. + * + * @author Vedran Pavic + * @since 5.1 + */ +public final class FeaturePolicyServerHttpHeadersWriter + implements ServerHttpHeadersWriter { + + public static final String FEATURE_POLICY = "Feature-Policy"; + + private ServerHttpHeadersWriter delegate; + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) + : Mono.empty(); + } + + /** + * Set the policy directive(s) to be used in the response header. + * + * @param policyDirectives the policy directive(s) + * @throws IllegalArgumentException if policyDirectives is {@code null} or empty + */ + public void setPolicyDirectives(String policyDirectives) { + Assert.hasLength(policyDirectives, "policyDirectives must not be null or empty"); + this.delegate = createDelegate(policyDirectives); + } + + private static ServerHttpHeadersWriter createDelegate(String policyDirectives) { + // @formatter:off + return StaticServerHttpHeadersWriter.builder() + .header(FEATURE_POLICY, policyDirectives) + .build(); + // @formatter:on + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..dcc9b6f5a1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/FeaturePolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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.server.header; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FeaturePolicyServerHttpHeadersWriter}. + * + * @author Vedran Pavic + */ +public class FeaturePolicyServerHttpHeadersWriterTests { + + private static final String DEFAULT_POLICY_DIRECTIVES = "geolocation 'self'"; + + private ServerWebExchange exchange; + + private FeaturePolicyServerHttpHeadersWriter writer; + + @Before + public void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new FeaturePolicyServerHttpHeadersWriter(); + } + + @Test + public void writeHeadersWhenUsingDefaultsThenDoesNotWrite() { + this.writer.writeHttpHeaders(this.exchange); + + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + public void writeHeadersWhenUsingPolicyThenWritesPolicy() { + this.writer.setPolicyDirectives(DEFAULT_POLICY_DIRECTIVES); + this.writer.writeHttpHeaders(this.exchange); + + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY)) + .containsOnly(DEFAULT_POLICY_DIRECTIVES); + } + + @Test + public void writeHeadersWhenAlreadyWrittenThenWritesHeader() { + this.writer.setPolicyDirectives(DEFAULT_POLICY_DIRECTIVES); + String headerValue = "camera: 'self'"; + this.exchange.getResponse().getHeaders() + .set(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY, headerValue); + this.writer.writeHttpHeaders(this.exchange); + + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY)) + .containsOnly(headerValue); + } + +}