Browse Source

Add Opaque Token WebTestClient Support

Fixes gh-7827
Josh Cummings 5 years ago
parent
commit
bd593a3431

+ 180 - 0
test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.test.web.reactive.server;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -26,7 +27,9 @@ import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
+import com.nimbusds.oauth2.sdk.util.StringUtils;
 import reactor.core.publisher.Mono;
 
 import org.springframework.core.convert.converter.Converter;
@@ -47,7 +50,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
@@ -58,8 +63,10 @@ import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
 import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
 import org.springframework.security.web.server.csrf.CsrfWebFilter;
 import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@@ -153,6 +160,20 @@ public class SecurityMockServerConfigurers {
 		return new JwtMutator();
 	}
 
+	/**
+	 * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
+	 * {@link BearerTokenAuthentication} for the
+	 * {@link Authentication} and an {@link OAuth2AuthenticatedPrincipal} for the
+	 * {@link Authentication#getPrincipal()}. All details are
+	 * declarative and do not require the token to be valid.
+	 *
+	 * @return the {@link OpaqueTokenMutator} to further configure or use
+	 * @since 5.3
+	 */
+	public static OpaqueTokenMutator mockOpaqueToken() {
+		return new OpaqueTokenMutator();
+	}
+
 	/**
 	 * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
 	 * {@link OAuth2AuthenticationToken} for the
@@ -516,6 +537,165 @@ public class SecurityMockServerConfigurers {
 		}
 	}
 
+	/**
+	 * @author Josh Cummings
+	 * @since 5.3
+	 */
+	public final static class OpaqueTokenMutator implements WebTestClientConfigurer, MockServerConfigurer {
+		private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
+		private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
+
+		private Supplier<OAuth2AuthenticatedPrincipal> principal = this::defaultPrincipal;
+
+		private OpaqueTokenMutator() { }
+
+		/**
+		 * Mutate the attributes using the given {@link Consumer}
+		 *
+		 * @param attributesConsumer The {@link Consumer} for mutating the {@Map} of attributes
+		 * @return the {@link OpaqueTokenMutator} for further configuration
+		 */
+		public OpaqueTokenMutator attributes(Consumer<Map<String, Object>> attributesConsumer) {
+			Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
+			this.attributes = () -> {
+				Map<String, Object> attributes = defaultAttributes();
+				attributesConsumer.accept(attributes);
+				return attributes;
+			};
+			this.principal = this::defaultPrincipal;
+			return this;
+		}
+
+		/**
+		 * Use the provided authorities in the resulting principal
+		 * @param authorities the authorities to use
+		 * @return the {@link OpaqueTokenMutator} for further configuration
+		 */
+		public OpaqueTokenMutator authorities(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(authorities, "authorities cannot be null");
+			this.authorities = () -> authorities;
+			this.principal = this::defaultPrincipal;
+			return this;
+		}
+
+		/**
+		 * Use the provided authorities in the resulting principal
+		 * @param authorities the authorities to use
+		 * @return the {@link OpaqueTokenMutator} for further configuration
+		 */
+		public OpaqueTokenMutator authorities(GrantedAuthority... authorities) {
+			Assert.notNull(authorities, "authorities cannot be null");
+			this.authorities = () -> Arrays.asList(authorities);
+			this.principal = this::defaultPrincipal;
+			return this;
+		}
+
+		/**
+		 * Use the provided scopes as the authorities in the resulting principal
+		 * @param scopes the scopes to use
+		 * @return the {@link OpaqueTokenMutator} for further configuration
+		 */
+		public OpaqueTokenMutator scopes(String... scopes) {
+			Assert.notNull(scopes, "scopes cannot be null");
+			this.authorities = () -> getAuthorities(Arrays.asList(scopes));
+			this.principal = this::defaultPrincipal;
+			return this;
+		}
+
+		/**
+		 * Use the provided principal
+		 * @param principal the principal to use
+		 * @return the {@link OpaqueTokenMutator} for further configuration
+		 */
+		public OpaqueTokenMutator principal(OAuth2AuthenticatedPrincipal principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = () -> principal;
+			return this;
+		}
+
+		@Override
+		public void beforeServerCreated(WebHttpHandlerBuilder builder) {
+			configurer().beforeServerCreated(builder);
+		}
+
+		@Override
+		public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
+			configurer().afterConfigureAdded(serverSpec);
+		}
+
+		@Override
+		public void afterConfigurerAdded(
+				WebTestClient.Builder builder,
+				@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
+				@Nullable ClientHttpConnector connector) {
+			httpHandlerBuilder.filter((exchange, chain) -> {
+				CsrfWebFilter.skipExchange(exchange);
+				return chain.filter(exchange);
+			});
+			configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector);
+		}
+
+		private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
+			OAuth2AuthenticatedPrincipal principal = this.principal.get();
+			OAuth2AccessToken accessToken = getOAuth2AccessToken(principal);
+			BearerTokenAuthentication token = new BearerTokenAuthentication
+					(principal, accessToken, principal.getAuthorities());
+			return mockAuthentication(token);
+		}
+
+		private Map<String, Object> defaultAttributes() {
+			Map<String, Object> attributes = new HashMap<>();
+			attributes.put(OAuth2IntrospectionClaimNames.SUBJECT, "user");
+			attributes.put(OAuth2IntrospectionClaimNames.SCOPE, "read");
+			return attributes;
+		}
+
+		private Collection<GrantedAuthority> defaultAuthorities() {
+			Map<String, Object> attributes = this.attributes.get();
+			Object scope = attributes.get(OAuth2IntrospectionClaimNames.SCOPE);
+			if (scope == null) {
+				return Collections.emptyList();
+			}
+			if (scope instanceof Collection) {
+				return getAuthorities((Collection) scope);
+			}
+			String scopes = scope.toString();
+			if (StringUtils.isBlank(scopes)) {
+				return Collections.emptyList();
+			}
+			return getAuthorities(Arrays.asList(scopes.split(" ")));
+		}
+
+		private OAuth2AuthenticatedPrincipal defaultPrincipal() {
+			return new DefaultOAuth2AuthenticatedPrincipal
+					(this.attributes.get(), this.authorities.get());
+		}
+
+		private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) {
+			return scopes.stream()
+					.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
+					.collect(Collectors.toList());
+		}
+
+		private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) {
+			Instant expiresAt = getInstant(principal.getAttributes(), "exp");
+			Instant issuedAt = getInstant(principal.getAttributes(), "iat");
+			return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+					"token", issuedAt, expiresAt);
+		}
+
+		private Instant getInstant(Map<String, Object> attributes, String name) {
+			Object value = attributes.get(name);
+			if (value == null) {
+				return null;
+			}
+			if (value instanceof Instant) {
+				return (Instant) value;
+			}
+			throw new IllegalArgumentException(name + " attribute must be of type Instant");
+		}
+	}
+
 	/**
 	 * @author Josh Cummings
 	 * @since 5.3

+ 171 - 0
test/src/test/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurerOpaqueTokenTests.java

@@ -0,0 +1,171 @@
+/*
+ * Copyright 2002-2020 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
+ *
+ *      https://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.test.web.reactive.server;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.springframework.core.ReactiveAdapterRegistry;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
+import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver;
+import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals.active;
+import static org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames.SUBJECT;
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken;
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
+
+/**
+ * @author Josh Cummings
+ * @since 5.3
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SecurityMockServerConfigurerOpaqueTokenTests extends AbstractMockServerConfigurersTests {
+	private GrantedAuthority authority1 = new SimpleGrantedAuthority("one");
+
+	private GrantedAuthority authority2 = new SimpleGrantedAuthority("two");
+
+	private WebTestClient client = WebTestClient
+			.bindToController(securityContextController)
+			.webFilter(new SecurityContextServerWebExchangeWebFilter())
+			.argumentResolvers(resolvers -> resolvers.addCustomResolver(
+					new CurrentSecurityContextArgumentResolver(new ReactiveAdapterRegistry())))
+			.apply(springSecurity())
+			.configureClient()
+			.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+			.build();
+
+	@Test
+	public void mockOpaqueTokenWhenUsingDefaultsThenBearerTokenAuthentication() {
+		this.client
+				.mutateWith(mockOpaqueToken())
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat(context.getAuthentication()).isInstanceOf(
+				BearerTokenAuthentication.class);
+		BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
+		assertThat(token.getAuthorities()).isNotEmpty();
+		assertThat(token.getToken()).isNotNull();
+		assertThat(token.getTokenAttributes().get(SUBJECT)).isEqualTo("user");
+	}
+
+	@Test
+	public void mockOpaqueTokenWhenAuthoritiesThenBearerTokenAuthentication() {
+		this.client
+				.mutateWith(mockOpaqueToken()
+						.authorities(this.authority1, this.authority2))
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
+				.containsOnly(this.authority1, this.authority2);
+	}
+
+	@Test
+	public void mockOpaqueTokenWhenScopesThenBearerTokenAuthentication() {
+		this.client
+				.mutateWith(mockOpaqueToken().scopes("scoped", "authorities"))
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
+				.containsOnly(new SimpleGrantedAuthority("SCOPE_scoped"),
+						new SimpleGrantedAuthority("SCOPE_authorities"));
+	}
+
+	@Test
+	public void mockOpaqueTokenWhenAttributesThenBearerTokenAuthentication() {
+		String sub = new String("my-subject");
+		this.client
+				.mutateWith(mockOpaqueToken()
+						.attributes(attributes -> attributes.put(SUBJECT, sub)))
+						.get()
+						.exchange()
+						.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
+		BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
+		assertThat(token.getTokenAttributes().get(SUBJECT)).isSameAs(sub);
+	}
+
+	@Test
+	public void mockOpaqueTokenWhenPrincipalThenBearerTokenAuthentication() {
+		OAuth2AuthenticatedPrincipal principal = active();
+		this.client
+				.mutateWith(mockOpaqueToken()
+						.principal(principal))
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
+		BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
+		assertThat(token.getPrincipal()).isSameAs(principal);
+	}
+
+	@Test
+	public void mockOpaqueTokenWhenPrincipalSpecifiedThenLastCalledTakesPrecedence() {
+		OAuth2AuthenticatedPrincipal principal = active(a -> a.put("scope", "user"));
+
+		this.client
+				.mutateWith(mockOpaqueToken()
+						.attributes(a -> a.put(SUBJECT, "foo"))
+						.principal(principal))
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		SecurityContext context = securityContextController.removeSecurityContext();
+		assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
+		BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
+		assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
+				.isEqualTo(principal.getAttribute(SUBJECT));
+
+		this.client
+				.mutateWith(mockOpaqueToken()
+						.principal(principal)
+						.attributes(a -> a.put(SUBJECT, "bar")))
+				.get()
+				.exchange()
+				.expectStatus().isOk();
+
+		context = securityContextController.removeSecurityContext();
+		assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
+		token = (BearerTokenAuthentication) context.getAuthentication();
+		assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
+				.isEqualTo("bar");
+	}
+}