diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java index c91099e4bf..8841937c51 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java @@ -28,15 +28,16 @@ import org.springframework.core.log.LogMessage; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.NullRememberMeServices; import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; @@ -105,7 +106,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { private String credentialsCharset = "UTF-8"; - private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); + private AuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); @@ -149,6 +150,18 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.securityContextRepository = securityContextRepository; } + /** + * Sets the + * {@link org.springframework.security.web.authentication.AuthenticationConverter} to + * use. Defaults to {@link BasicAuthenticationConverter} + * @param authenticationConverter the converter to use + * @since 6.2 + */ + public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + } + @Override public void afterPropertiesSet() { Assert.notNull(this.authenticationManager, "An AuthenticationManager is required"); @@ -161,7 +174,7 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { - UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request); + Authentication authRequest = this.authenticationConverter.convert(request); if (authRequest == null) { this.logger.trace("Did not process authentication request since failed to find " + "username and password in Basic Authorization header"); @@ -250,9 +263,19 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.securityContextHolderStrategy = securityContextHolderStrategy; } + /** + * Sets the {@link AuthenticationDetailsSource} to use. By default, it is set to use + * the {@link WebAuthenticationDetailsSource}. Note that this configuration applies + * exclusively when the {@link #authenticationConverter} is set to + * {@link BasicAuthenticationConverter}. If you are utilizing a different + * implementation, you will need to manually specify the authentication details on it. + * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. + */ public void setAuthenticationDetailsSource( AuthenticationDetailsSource authenticationDetailsSource) { - this.authenticationConverter.setAuthenticationDetailsSource(authenticationDetailsSource); + if (this.authenticationConverter instanceof BasicAuthenticationConverter basicAuthenticationConverter) { + basicAuthenticationConverter.setAuthenticationDetailsSource(authenticationDetailsSource); + } } public void setRememberMeServices(RememberMeServices rememberMeServices) { @@ -260,10 +283,20 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.rememberMeServices = rememberMeServices; } + /** + * Sets the charset to use when decoding credentials to {@link String}s. By default, + * it is set to {@code UTF-8}. Note that this configuration applies exclusively when + * the {@link #authenticationConverter} is set to + * {@link BasicAuthenticationConverter}. If you are utilizing a different + * implementation, you will need to manually specify the charset on it. + * @param credentialsCharset the charset to use. + */ public void setCredentialsCharset(String credentialsCharset) { Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty"); this.credentialsCharset = credentialsCharset; - this.authenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset)); + if (this.authenticationConverter instanceof BasicAuthenticationConverter basicAuthenticationConverter) { + basicAuthenticationConverter.setCredentialsCharset(Charset.forName(credentialsCharset)); + } } protected String getCredentialsCharset(HttpServletRequest httpRequest) { diff --git a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java index 134a5d46f3..faf9f17db4 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -41,9 +42,12 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.test.web.CodecTestUtils; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -488,4 +492,57 @@ public class BasicAuthenticationFilterTests { assertThat(authenticationRequest.getName()).isEqualTo("rod"); } + @Test + public void doFilterWhenCustomAuthenticationConverterThatIgnoresRequestThenIgnores() throws Exception { + this.filter.setAuthenticationConverter(new TestAuthenticationConverter()); + String token = "rod:koala"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); + request.setServletPath("/ignored"); + FilterChain filterChain = mock(FilterChain.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + this.filter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(200); + + verify(this.manager, never()).authenticate(any(Authentication.class)); + verify(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + verifyNoMoreInteractions(this.manager, filterChain); + } + + @Test + public void doFilterWhenCustomAuthenticationConverterRequestThenAuthenticate() throws Exception { + this.filter.setAuthenticationConverter(new TestAuthenticationConverter()); + String token = "rod:koala"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic " + CodecTestUtils.encodeBase64(token)); + request.setServletPath("/ok"); + FilterChain filterChain = mock(FilterChain.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + this.filter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull(); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("rod"); + } + + @Test + public void setAuthenticationConverterWhenNullThenException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationConverter(null)); + } + + static class TestAuthenticationConverter implements AuthenticationConverter { + + private final RequestMatcher matcher = AntPathRequestMatcher.antMatcher("/ignored"); + + private final BasicAuthenticationConverter delegate = new BasicAuthenticationConverter(); + + @Override + public Authentication convert(HttpServletRequest request) { + if (this.matcher.matches(request)) { + return null; + } + return this.delegate.convert(request); + } + + } + }