diff --git a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java index 5087afe14d..12d8125c1b 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/HttpSecurity.java @@ -29,6 +29,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authorization.AuthorizationContext; import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; +import org.springframework.security.web.server.context.AuthenticationReactorContextFilter; import org.springframework.security.web.server.context.SecurityContextRepositoryWebFilter; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; import org.springframework.security.web.server.context.SecurityContextRepository; @@ -114,6 +115,7 @@ public class HttpSecurity { securityContextRepository.ifPresent( scr -> httpBasic.securityContextRepository(scr)) ; filters.add(httpBasic.build()); } + filters.add(new AuthenticationReactorContextFilter()); if(authorizeExchangeBuilder != null) { filters.add(new ExceptionTranslationWebFilter()); filters.add(authorizeExchangeBuilder.build()); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index 74a05df865..1c926bb0f1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -25,7 +25,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.web.server.HttpSecurity; +import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.MapUserDetailsRepository; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsRepository; @@ -35,8 +39,14 @@ import org.springframework.security.web.server.WebFilterChainFilter; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; -import static org.mockito.Mockito.mock; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials; +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; /** * @author Rob Winch @@ -60,6 +70,50 @@ public class EnableWebFluxSecurityTests { .expectBody().isEmpty(); } + @Test + public void defaultPopulatesReactorContext() { + Principal currentPrincipal = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + WebTestClient client = WebTestClientBuilder.bindToWebFilters( + (exchange, chain) -> + chain.filter(exchange.mutate().principal(Mono.just(currentPrincipal)).build()), + springSecurityFilterChain, + (exchange,chain) -> + Mono.currentContext() + .flatMap( c -> c.>get(Authentication.class)) + .flatMap( principal -> exchange.getResponse() + .writeWith(Mono.just(toDataBuffer(principal.getName())))) + ).build(); + + client + .get() + .uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo(currentPrincipal.getName())); + } + + @Test + public void defaultPopulatesReactorContextWhenAuthenticating() { + WebTestClient client = WebTestClientBuilder.bindToWebFilters( + springSecurityFilterChain, + (exchange,chain) -> + Mono.currentContext() + .flatMap( c -> c.>get(Authentication.class)) + .flatMap( principal -> exchange.getResponse() + .writeWith(Mono.just(toDataBuffer(principal.getName())))) + ) + .filter(basicAuthentication()) + .build(); + + client + .get() + .uri("/") + .attributes(basicAuthenticationCredentials("user","password")) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).consumeWith( result -> assertThat(result.getResponseBody()).isEqualTo("user")); + } + @EnableWebFluxSecurity static class Config { @Bean @@ -121,4 +175,10 @@ public class EnableWebFluxSecurityTests { } } } + + private static DataBuffer toDataBuffer(String body) { + DataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer(); + buffer.write(body.getBytes(StandardCharsets.UTF_8)); + return buffer; + } } diff --git a/webflux/src/main/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilter.java b/webflux/src/main/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilter.java new file mode 100644 index 0000000000..4ae287f6b5 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilter.java @@ -0,0 +1,45 @@ +/* + * + * * 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.server.context; + +import org.springframework.security.core.Authentication; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.security.Principal; + +/** + * Populate the {@link Principal} from {@link ServerWebExchange#getPrincipal()} into the + * Reactor {@link Context}. + * + * @author Rob Winch + * @since 5.0 + */ +public class AuthenticationReactorContextFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + return chain.filter(exchange) + .contextStart((Context context) -> context.put(Authentication.class, exchange.getPrincipal())); + } +} diff --git a/webflux/src/test/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilterTests.java b/webflux/src/test/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilterTests.java new file mode 100644 index 0000000000..e48bae1367 --- /dev/null +++ b/webflux/src/test/java/org/springframework/security/web/server/context/AuthenticationReactorContextFilterTests.java @@ -0,0 +1,94 @@ +/* + * + * * 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.server.context; + +import org.junit.Test; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.handler.DefaultWebFilterChain; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.context.Context; + +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class AuthenticationReactorContextFilterTests { + AuthenticationReactorContextFilter filter = new AuthenticationReactorContextFilter(); + + Principal principal = new TestingAuthenticationToken("user","password", "ROLE_USER"); + + ServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange(); + + @Test + public void filterWhenExistingContextAndPrincipalNotNullThenContextPopulated() { + exchange = exchange.mutate().principal(Mono.just(principal)).build(); + StepVerifier.create(filter.filter(exchange, + new DefaultWebFilterChain( e -> + Mono.currentContext().doOnSuccess( context -> { + Principal contextPrincipal = context.>get(Authentication.class).block(); + assertThat(contextPrincipal).isEqualTo(principal); + assertThat(context.get("foo")).isEqualTo("bar"); + }) + .then() + ) + ) + .contextStart( context -> context.put("foo", "bar"))) + .verifyComplete(); + } + + @Test + public void filterWhenPrincipalNotNullThenContextPopulated() { + exchange = exchange.mutate().principal(Mono.just(principal)).build(); + StepVerifier.create(filter.filter(exchange, + new DefaultWebFilterChain( e -> + Mono.currentContext().doOnSuccess( context -> { + Principal contextPrincipal = context.>get(Authentication.class).block(); + assertThat(contextPrincipal).isEqualTo(principal); + }) + .then() + ) + )) + .verifyComplete(); + } + + @Test + public void filterWhenPrincipalNullThenContextEmpty() { + Context defaultContext = Context.empty(); + StepVerifier.create(filter.filter(exchange, + new DefaultWebFilterChain( e -> + Mono.currentContext() + .defaultIfEmpty(defaultContext) + .doOnSuccess( context -> { + Principal contextPrincipal = context.>get(Authentication.class).block(); + assertThat(contextPrincipal).isNull(); + }) + .then() + ) + )) + .verifyComplete(); + } +}