diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index 6e1f1db678..cfc70becde 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -48,6 +48,10 @@ public enum SecurityWebFiltersOrder { */ FORM_LOGIN, AUTHENTICATION, + /** + * Instance of AnonymousAuthenticationWebFilter + */ + ANONYMOUS_AUTHENTICATION, OAUTH2_AUTHORIZATION_CODE, LOGIN_PAGE_GENERATING, LOGOUT_PAGE_GENERATING, 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 ca75c30d3c..d469d8627f 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 @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.UUID; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -158,6 +159,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 org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; /** * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux. @@ -264,6 +268,8 @@ public class ServerHttpSecurity { private Throwable built; + private AnonymousSpec anonymous; + /** * The ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance. * @@ -425,6 +431,29 @@ public class ServerHttpSecurity { return this.cors; } + /** + * @since 5.2.0 + * @author Ankur Pathak + * Enables and Configures annonymous authentication. Anonymous Authentication is disabled by default. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 *      http
+	 *          // ...
+	 *          .anonymous().key("key")
+	 *          .authorities("ROLE_ANONYMOUS");
+	 *      return http.build();
+	 *  }
+	 * 
+ */ + public AnonymousSpec anonymous(){ + if (this.anonymous == null) { + this.anonymous = new AnonymousSpec(); + } + return this.anonymous; + } + /** * Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the * correct order. @@ -1356,6 +1385,9 @@ public class ServerHttpSecurity { if (this.client != null) { this.client.configure(this); } + if (this.anonymous != null) { + this.anonymous.configure(this); + } this.loginPage.configure(this); if (this.logout != null) { this.logout.configure(this); @@ -2589,4 +2621,124 @@ public class ServerHttpSecurity { .subscriberContext(Context.of(ServerWebExchange.class, exchange)); } } + + /** + * Configures annonymous authentication + * @author Ankur Pathak + * @since 5.2.0 + */ + public final class AnonymousSpec { + private String key; + private AnonymousAuthenticationWebFilter authenticationFilter; + private Object principal = "anonymousUser"; + private List authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"); + + /** + * Sets the key to identify tokens created for anonymous authentication. Default is a + * secure randomly generated key. + * + * @param key the key to identify tokens created for anonymous authentication. Default + * is a secure randomly generated key. + * @return the {@link AnonymousSpec} for further customization of anonymous + * authentication + */ + public AnonymousSpec key(String key) { + this.key = key; + return this; + } + + /** + * Sets the principal for {@link Authentication} objects of anonymous users + * + * @param principal used for the {@link Authentication} object of anonymous users + * @return the {@link AnonymousSpec} for further customization of anonymous + * authentication + */ + public AnonymousSpec principal(Object principal) { + this.principal = principal; + return this; + } + + /** + * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} + * for anonymous users + * + * @param authorities Sets the + * {@link org.springframework.security.core.Authentication#getAuthorities()} for + * anonymous users + * @return the {@link AnonymousSpec} for further customization of anonymous + * authentication + */ + public AnonymousSpec authorities(List authorities) { + this.authorities = authorities; + return this; + } + + /** + * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()} + * for anonymous users + * + * @param authorities Sets the + * {@link org.springframework.security.core.Authentication#getAuthorities()} for + * anonymous users (i.e. "ROLE_ANONYMOUS") + * @return the {@link AnonymousSpec} for further customization of anonymous + * authentication + */ + public AnonymousSpec authorities(String... authorities) { + return authorities(AuthorityUtils.createAuthorityList(authorities)); + } + + /** + * Sets the {@link AnonymousAuthenticationWebFilter} used to populate an anonymous user. + * If this is set, no attributes on the {@link AnonymousSpec} will be set on the + * {@link AnonymousAuthenticationWebFilter}. + * + * @param authenticationFilter the {@link AnonymousAuthenticationWebFilter} used to + * populate an anonymous user. + * + * @return the {@link AnonymousSpec} for further customization of anonymous + * authentication + */ + public AnonymousSpec authenticationFilter( + AnonymousAuthenticationWebFilter authenticationFilter) { + this.authenticationFilter = authenticationFilter; + return this; + } + + /** + * Allows method chaining to continue configuring the {@link ServerHttpSecurity} + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity and() { + return ServerHttpSecurity.this; + } + + /** + * Disables anonymous authentication. + * @return the {@link ServerHttpSecurity} to continue configuring + */ + public ServerHttpSecurity disable() { + ServerHttpSecurity.this.anonymous = null; + return ServerHttpSecurity.this; + } + + protected void configure(ServerHttpSecurity http) { + if (authenticationFilter == null) { + authenticationFilter = new AnonymousAuthenticationWebFilter(getKey(), principal, + authorities); + } + http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION); + } + + private String getKey() { + if (key == null) { + key = UUID.randomUUID().toString(); + } + return key; + } + + + private AnonymousSpec() {} + + } } 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 eb10e23464..720312d493 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 @@ -34,8 +34,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; -import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; @@ -63,6 +61,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; +import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests; /** * @author Rob Winch @@ -216,6 +217,44 @@ public class ServerHttpSecurityTests { } + @Test + public void anonymous(){ + SecurityWebFilterChain securityFilterChain = this.http.anonymous().and().build(); + WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(AnonymousAuthenticationWebFilterTests.HttpMeController.class, + securityFilterChain).build(); + + client.get() + .uri("/me") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("anonymousUser"); + + } + + @Test + public void basicWithAnonymous() { + given(this.authenticationManager.authenticate(any())).willReturn(Mono.just(new TestingAuthenticationToken("rob", "rob", "ROLE_USER", "ROLE_ADMIN"))); + + this.http.securityContextRepository(new WebSessionServerSecurityContextRepository()); + this.http.httpBasic().and().anonymous(); + this.http.authenticationManager(this.authenticationManager); + ServerHttpSecurity.AuthorizeExchangeSpec authorize = this.http.authorizeExchange(); + authorize.anyExchange().hasAuthority("ROLE_ADMIN"); + + WebTestClient client = buildClient(); + + EntityExchangeResult result = client.get() + .uri("/") + .headers(headers -> headers.setBasicAuth("rob", "rob")) + .exchange() + .expectStatus().isOk() + .expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+") + .expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok")) + .returnResult(); + + assertThat(result.getResponseCookies().getFirst("SESSION")).isNull(); + } + private Optional getWebFilter(SecurityWebFilterChain filterChain, Class filterClass) { return (Optional) filterChain.getWebFilters() .filter(Objects::nonNull) @@ -242,7 +281,6 @@ public class ServerHttpSecurityTests { } private static class TestWebFilter implements WebFilter { - @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java new file mode 100644 index 0000000000..15414d2f28 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java @@ -0,0 +1,94 @@ +/* + * 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.authentication; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; + +/** + * Detects if there is no {@code Authentication} object in the + * {@code ReactiveSecurityContextHolder}, and populates it with one if needed. + * + * @author Ankur Pathak + * @since 5.2.0 + */ +public class AnonymousAuthenticationWebFilter implements WebFilter { + // ~ Instance fields + // ================================================================================================ + + private String key; + private Object principal; + private List authorities; + + /** + * Creates a filter with a principal named "anonymousUser" and the single authority + * "ROLE_ANONYMOUS". + * + * @param key the key to identify tokens created by this filter + */ + public AnonymousAuthenticationWebFilter(String key) { + this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + } + + /** + * @param key key the key to identify tokens created by this filter + * @param principal the principal which will be used to represent anonymous users + * @param authorities the authority list for anonymous users + */ + public AnonymousAuthenticationWebFilter(String key, Object principal, + List authorities) { + Assert.hasLength(key, "key cannot be null or empty"); + Assert.notNull(principal, "Anonymous authentication principal must be set"); + Assert.notNull(authorities, "Anonymous authorities must be set"); + this.key = key; + this.principal = principal; + this.authorities = authorities; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return ReactiveSecurityContextHolder.getContext() + .switchIfEmpty(Mono.defer(() -> { + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(createAuthentication(exchange)); + return chain.filter(exchange) + .subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) + .then(Mono.empty()); + })).flatMap(securityContext -> chain.filter(exchange)); + + } + + protected Authentication createAuthentication(ServerWebExchange exchange) { + AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, + principal, authorities); + return auth; + } +} diff --git a/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java b/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java index 2ed8654b8b..84580c698e 100644 --- a/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java +++ b/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java @@ -43,6 +43,14 @@ public class WebTestClientBuilder { return bindToWebFilters(new WebFilterChainProxy(securityWebFilterChain)); } + public static Builder bindToControllerAndWebFilters(Class controller, WebFilter... webFilters) { + return WebTestClient.bindToController(controller).webFilter(webFilters).configureClient(); + } + + public static Builder bindToControllerAndWebFilters(Class controller, SecurityWebFilterChain securityWebFilterChain) { + return bindToControllerAndWebFilters(controller, new WebFilterChainProxy(securityWebFilterChain)); + } + @RestController public static class Http200RestController { @RequestMapping("/**") @@ -51,4 +59,5 @@ public class WebTestClientBuilder { return "ok"; } } + } diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java new file mode 100644 index 0000000000..86ceb18ac9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java @@ -0,0 +1,70 @@ +/* + * 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.authentication; + +import java.util.UUID; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import reactor.core.publisher.Mono; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; + +/** + * @author Ankur Pathak + * @since 5.2.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AnonymousAuthenticationWebFilterTests { + + @Test + public void anonymousAuthenticationFilterWorking() { + + WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(HttpMeController.class, + new AnonymousAuthenticationWebFilter(UUID.randomUUID().toString())) + .build(); + + client.get() + .uri("/me") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("anonymousUser"); + } + + @RestController + @RequestMapping("/me") + public static class HttpMeController { + @GetMapping + public Mono me(ServerWebExchange exchange) { + return ReactiveSecurityContextHolder + .getContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .ofType(String.class); + } + } +}