Browse Source

Added support for Anonymous Authentication

1. Created new WebFilter AnonymousAuthenticationWebFilter to
for anonymous authentication
2. Created class AnonymousSpec, method anonymous to configure
anonymous authentication in ServerHttpSecurity
3. Added ANONYMOUS_AUTHENTICATION order after AUTHENTICATION for
anonymous authentication in SecurityWebFiltersOrder
4. Added tests for anonymous authentication in
AnonymousAuthenticationWebFilterTests and ServerHttpSecurityTests
5. Added support for Controller in WebTestClientBuilder

Fixes: gh-5934
Ankur Pathak 6 years ago
parent
commit
2b369cfe98

+ 4 - 0
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,

+ 152 - 0
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.
+	 *
+	 * <pre class="code">
+	 *  &#064;Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 *      http
+	 *          // ...
+	 *          .anonymous().key("key")
+	 *          .authorities("ROLE_ANONYMOUS");
+	 *      return http.build();
+	 *  }
+	 * </pre>
+	 */
+	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<GrantedAuthority> 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<GrantedAuthority> 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() {}
+
+	}
 }

+ 41 - 3
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<String> 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 <T extends WebFilter> Optional<T> getWebFilter(SecurityWebFilterChain filterChain, Class<T> filterClass) {
 		return (Optional<T>) filterChain.getWebFilters()
 				.filter(Objects::nonNull)
@@ -242,7 +281,6 @@ public class ServerHttpSecurityTests {
 	}
 
 	private static class TestWebFilter implements WebFilter {
-
 		@Override
 		public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
 			return chain.filter(exchange);

+ 94 - 0
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<GrantedAuthority> 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<GrantedAuthority> 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<Void> 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;
+	}
+}

+ 9 - 0
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";
 		}
 	}
+
 }

+ 70 - 0
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<String> me(ServerWebExchange exchange) {
+			return ReactiveSecurityContextHolder
+					.getContext()
+					.map(SecurityContext::getAuthentication)
+					.map(Authentication::getPrincipal)
+					.ofType(String.class);
+		}
+	}
+}