Ver Fonte

Check If toBuilder Is Implemented

Since RC1 is right around the corner, let's change the API
footprint as little as possible by using reflection to check
if a class has declared toBuilder themselves. If they have, we
can assume that that class's builder will produce that class.

Issue gh-18052
Josh Cummings há 1 dia atrás
pai
commit
b1a50a25b6
13 ficheiros alterados com 214 adições e 6 exclusões
  1. 25 0
      core/src/test/java/org/springframework/security/authentication/NonBuildableAuthenticationToken.java
  2. 7 2
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java
  3. 19 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java
  4. 11 1
      web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
  5. 11 1
      web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java
  6. 11 1
      web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java
  7. 11 1
      web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java
  8. 13 0
      web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java
  9. 18 0
      web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java
  10. 22 0
      web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java
  11. 18 0
      web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java
  12. 21 0
      web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java
  13. 27 0
      web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java

+ 25 - 0
core/src/test/java/org/springframework/security/authentication/NonBuildableAuthenticationToken.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright 2004-present 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.authentication;
+
+public class NonBuildableAuthenticationToken extends TestingAuthenticationToken {
+
+	public NonBuildableAuthenticationToken(String user, String password, String... authorities) {
+		super(user, password, authorities);
+	}
+
+}

+ 7 - 2
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.oauth2.server.resource.web.authentication;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -56,7 +57,6 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextR
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
-import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
@@ -214,7 +214,12 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
 	}
 
 	private static boolean declaresToBuilder(Authentication authentication) {
-		return ReflectionUtils.findMethod(authentication.getClass(), "toBuilder") != null;
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
 	}
 
 	/**

+ 19 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java

@@ -38,6 +38,7 @@ import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
 import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -316,6 +317,24 @@ public class BearerTokenAuthenticationFilterTests {
 		// @formatter:on
 	}
 
+	@Test
+	void doFilterWhenNonBuildableAuthenticationSubclassThenSkipsToBuilder() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		given(this.bearerTokenResolver.resolve(any())).willReturn("token");
+		BearerTokenAuthenticationFilter filter = addMocks(
+				new BearerTokenAuthenticationFilter(this.authenticationManager));
+		filter.doFilter(this.request, this.response, this.filterChain);
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		// @formatter:off
+		SecurityAssertions.assertThat(authentication).authorities()
+				.extracting(GrantedAuthority::getAuthority)
+				.containsExactly("FACTORTWO");
+		// @formatter:on
+	}
+
 	@Test
 	public void setAuthenticationEntryPointWhenNullThenThrowsException() {
 		BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager);

+ 11 - 1
web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.authentication;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -252,7 +253,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 				return;
 			}
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
-			if (current != null && current.isAuthenticated()) {
+			if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
 				authenticationResult = authenticationResult.toBuilder()
 				// @formatter:off
 					.authorities((a) -> {
@@ -285,6 +286,15 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 		}
 	}
 
+	private static boolean declaresToBuilder(Authentication authentication) {
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	/**
 	 * Indicates whether this filter should attempt to process a login request for the
 	 * current invocation.

+ 11 - 1
web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.authentication;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -188,7 +189,7 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 				return;
 			}
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
-			if (current != null && current.isAuthenticated()) {
+			if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
 				authenticationResult = authenticationResult.toBuilder()
 				// @formatter:off
 					.authorities((a) -> {
@@ -215,6 +216,15 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 		}
 	}
 
+	private static boolean declaresToBuilder(Authentication authentication) {
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	@Override
 	protected String getAlreadyFilteredAttributeName() {
 		String name = getFilterName();

+ 11 - 1
web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.authentication.preauth;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -208,7 +209,7 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 			authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
 			Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
-			if (current != null && current.isAuthenticated()) {
+			if (current != null && current.isAuthenticated() && declaresToBuilder(authenticationResult)) {
 				authenticationResult = authenticationResult.toBuilder()
 				// @formatter:off
 					.authorities((a) -> {
@@ -234,6 +235,15 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 		}
 	}
 
+	private static boolean declaresToBuilder(Authentication authentication) {
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	/**
 	 * Puts the <code>Authentication</code> instance returned by the authentication
 	 * manager into the secure context.

+ 11 - 1
web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.authentication.www;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.nio.charset.Charset;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -190,7 +191,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 			if (authenticationIsRequired(username)) {
 				Authentication authResult = this.authenticationManager.authenticate(authRequest);
 				Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
-				if (current != null && current.isAuthenticated()) {
+				if (current != null && current.isAuthenticated() && declaresToBuilder(authResult)) {
 					authResult = authResult.toBuilder()
 					// @formatter:off
 						.authorities((a) -> {
@@ -234,6 +235,15 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 		chain.doFilter(request, response);
 	}
 
+	private static boolean declaresToBuilder(Authentication authentication) {
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	protected boolean authenticationIsRequired(String username) {
 		// Only reauthenticate if username doesn't match SecurityContextHolder and user
 		// isn't authenticated (see SEC-53)

+ 13 - 0
web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.web.server.authentication;
 
+import java.lang.reflect.Method;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -141,6 +142,9 @@ public class AuthenticationWebFilter implements WebFilter {
 			if (!current.isAuthenticated()) {
 				return result;
 			}
+			if (!declaresToBuilder(result)) {
+				return result;
+			}
 			return result.toBuilder()
 			// @formatter:off
 				.authorities((a) -> {
@@ -158,6 +162,15 @@ public class AuthenticationWebFilter implements WebFilter {
 		}).switchIfEmpty(Mono.just(result));
 	}
 
+	private static boolean declaresToBuilder(Authentication authentication) {
+		for (Method method : authentication.getClass().getDeclaredMethods()) {
+			if (method.getName().equals("toBuilder") && method.getParameterTypes().length == 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
 		ServerWebExchange exchange = webFilterExchange.getExchange();
 		SecurityContextImpl securityContext = new SecurityContextImpl();

+ 18 - 0
web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java

@@ -37,6 +37,8 @@ import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -480,6 +482,22 @@ public class AbstractAuthenticationProcessingFilterTests {
 			.containsExactly(DefaultEqualsGrantedAuthority.AUTHORITY);
 	}
 
+	@Test
+	void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		MockHttpServletRequest request = createMockAuthenticationRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockAuthenticationFilter filter = new MockAuthenticationFilter(
+				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		filter.doFilter(request, response, new MockFilterChain(false));
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		SecurityAssertions.assertThat(authentication)
+			.authorities()
+			.extracting(GrantedAuthority::getAuthority)
+			.containsExactly("FACTORTWO");
+	}
+
 	/**
 	 * https://github.com/spring-projects/spring-security/pull/3905
 	 */

+ 22 - 0
web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java

@@ -37,6 +37,8 @@ import org.springframework.mock.web.MockHttpSession;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
@@ -348,6 +350,26 @@ public class AuthenticationFilterTests {
 			.containsExactlyInAnyOrder(DefaultEqualsGrantedAuthority.AUTHORITY);
 	}
 
+	@Test
+	void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		given(this.authenticationConverter.convert(any())).willReturn(existingAuthn);
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(new NonBuildableAuthenticationToken("user", "password", "FACTORTWO"));
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain chain = new MockFilterChain();
+		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
+				this.authenticationConverter);
+		filter.doFilter(request, response, chain);
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		SecurityAssertions.assertThat(authentication)
+			.authorities()
+			.extracting(GrantedAuthority::getAuthority)
+			.containsExactly("FACTORTWO");
+	}
+
 	@Test
 	public void filterWhenCustomSecurityContextRepositoryAndSuccessfulAuthenticationRepositoryUsed() throws Exception {
 		SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);

+ 18 - 0
web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java

@@ -32,6 +32,8 @@ import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -436,6 +438,22 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		// @formatter:on
 	}
 
+	@Test
+	void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.filter = createFilterAuthenticatesWith(
+				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		this.filter.doFilter(request, response, new MockFilterChain());
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		SecurityAssertions.assertThat(authentication)
+			.authorities()
+			.extracting(GrantedAuthority::getAuthority)
+			.containsExactly("FACTORTWO");
+	}
+
 	private AbstractPreAuthenticatedProcessingFilter createFilterAuthenticatesWith(Authentication authentication) {
 		ConcretePreAuthenticatedProcessingFilter filter = new ConcretePreAuthenticatedProcessingFilter();
 		filter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);

+ 21 - 0
web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java

@@ -36,6 +36,8 @@ import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpSession;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -539,6 +541,25 @@ public class BasicAuthenticationFilterTests {
 			.containsExactly(DefaultEqualsGrantedAuthority.AUTHORITY);
 	}
 
+	@Test
+	void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + CodecTestUtils.encodeBase64("a:b"));
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		AuthenticationManager manager = mock(AuthenticationManager.class);
+		given(manager.authenticate(any()))
+			.willReturn(new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
+		filter.doFilter(request, response, new MockFilterChain());
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		SecurityAssertions.assertThat(authentication)
+			.authorities()
+			.extracting(GrantedAuthority::getAuthority)
+			.containsExactly("FACTORTWO");
+	}
+
 	@Test
 	public void doFilterWhenCustomAuthenticationConverterRequestThenAuthenticate() throws Exception {
 		this.filter.setAuthenticationConverter(new TestAuthenticationConverter());

+ 27 - 0
web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java

@@ -25,8 +25,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import reactor.core.publisher.Mono;
 
 import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.NonBuildableAuthenticationToken;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
+import org.springframework.security.authentication.SecurityAssertions;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
@@ -230,6 +232,31 @@ public class AuthenticationWebFilterTests {
 			.containsExactly(DefaultEqualsGrantedAuthority.AUTHORITY);
 	}
 
+	@Test
+	void doFilterWhenNotOverridingToBuilderThenDoesNotMergeAuthorities() throws Exception {
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password", "FACTORONE");
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(Mono.just(new NonBuildableAuthenticationToken("user", "password", "FACTORTWO")));
+		given(this.securityContextRepository.save(any(), any())).willReturn(Mono.empty());
+		this.filter = new AuthenticationWebFilter(this.authenticationManager);
+		this.filter.setSecurityContextRepository(this.securityContextRepository);
+		WebTestClient client = WebTestClientBuilder.bindToWebFilters(new RunAsWebFilter(existingAuthn), this.filter)
+			.build();
+		client.get()
+			.uri("/")
+			.headers((headers) -> headers.setBasicAuth("test", "this"))
+			.exchange()
+			.expectStatus()
+			.isOk();
+		ArgumentCaptor<SecurityContext> context = ArgumentCaptor.forClass(SecurityContext.class);
+		verify(this.securityContextRepository).save(any(), context.capture());
+		Authentication authentication = context.getValue().getAuthentication();
+		SecurityAssertions.assertThat(authentication)
+			.authorities()
+			.extracting(GrantedAuthority::getAuthority)
+			.containsExactly("FACTORTWO");
+	}
+
 	@Test
 	public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationFailThenUnauthorized() {
 		given(this.authenticationManager.authenticate(any()))