1
0
mirror of synced 2026-05-22 13:23:17 +00:00

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
This commit is contained in:
Josh Cummings
2025-10-16 13:22:18 -06:00
parent 4281f6b00b
commit b1a50a25b6
13 changed files with 214 additions and 6 deletions
@@ -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);
}
}
@@ -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;
}
/**
@@ -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);
@@ -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.
@@ -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();
@@ -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.
@@ -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)
@@ -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();
@@ -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
*/
@@ -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);
@@ -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);
@@ -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());
@@ -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()))