Переглянути джерело

MFA is now Opt In

This commit ensures that MFA is only performed when users opt in. By
doing so, we allow users to decide if they will opt into the semantics
of merging two Authentication instances.

Closes gh-18126
Rob Winch 6 днів тому
батько
коміт
aaf738f7ac
14 змінених файлів з 472 додано та 4 видалено
  1. 2 2
      config/src/main/java/org/springframework/security/config/annotation/authorization/AuthorizationManagerFactoryConfiguration.java
  2. 4 2
      config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java
  3. 65 0
      config/src/main/java/org/springframework/security/config/annotation/authorization/EnableMfaFiltersConfiguration.java
  4. 50 0
      config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationSelector.java
  5. 158 0
      config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationFiltersSetTests.java
  6. 54 0
      config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationTests.java
  7. 14 0
      web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
  8. 14 0
      web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java
  9. 14 0
      web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java
  10. 14 0
      web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java
  11. 20 0
      web/src/test/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilterTests.java
  12. 24 0
      web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java
  13. 19 0
      web/src/test/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilterTests.java
  14. 20 0
      web/src/test/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilterTests.java

+ 2 - 2
config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationConfiguration.java → config/src/main/java/org/springframework/security/config/annotation/authorization/AuthorizationManagerFactoryConfiguration.java

@@ -34,7 +34,7 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
  * @since 7.0
  * @since 7.0
  * @see EnableGlobalMultiFactorAuthentication
  * @see EnableGlobalMultiFactorAuthentication
  */
  */
-class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
+class AuthorizationManagerFactoryConfiguration implements ImportAware {
 
 
 	private String[] authorities;
 	private String[] authorities;
 
 
@@ -51,7 +51,7 @@ class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
 		Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
 		Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
 			.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
 			.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
 
 
-		this.authorities = (String[]) multiFactorAuthenticationAttrs.get("authorities");
+		this.authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
 	}
 	}
 
 
 }
 }

+ 4 - 2
config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java

@@ -51,13 +51,15 @@ import org.springframework.security.authorization.DefaultAuthorizationManagerFac
 @Retention(RetentionPolicy.RUNTIME)
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @Target(ElementType.TYPE)
 @Documented
 @Documented
-@Import(GlobalMultiFactorAuthenticationConfiguration.class)
+@Import(GlobalMultiFactorAuthenticationSelector.class)
 public @interface EnableGlobalMultiFactorAuthentication {
 public @interface EnableGlobalMultiFactorAuthentication {
 
 
 	/**
 	/**
 	 * The additional authorities that are required.
 	 * The additional authorities that are required.
 	 * @return the additional authorities that are required (e.g. {
 	 * @return the additional authorities that are required (e.g. {
-	 * FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD })
+	 * FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD }). Can
+	 * be null or an empty array if no additional authorities are required (if
+	 * authorization rules are not globally requiring MFA).
 	 * @see org.springframework.security.core.authority.FactorGrantedAuthority
 	 * @see org.springframework.security.core.authority.FactorGrantedAuthority
 	 */
 	 */
 	String[] authorities();
 	String[] authorities();

+ 65 - 0
config/src/main/java/org/springframework/security/config/annotation/authorization/EnableMfaFiltersConfiguration.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-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.config.annotation.authorization;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+
+@Configuration(proxyBeanMethods = false)
+class EnableMfaFiltersConfiguration {
+
+	@Bean
+	BeanPostProcessor mfaBeanPostProcessor() {
+		return new EnableMfaFiltersPostProcessor();
+	}
+
+	/**
+	 * A {@link BeanPostProcessor} that enables MFA on authentication filters.
+	 *
+	 * @author Rob Winch
+	 * @since 7.0
+	 */
+	private static class EnableMfaFiltersPostProcessor implements BeanPostProcessor {
+
+		@Override
+		public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+			if (bean instanceof AbstractAuthenticationProcessingFilter filter) {
+				filter.setMfaEnabled(true);
+			}
+			if (bean instanceof AuthenticationFilter filter) {
+				filter.setMfaEnabled(true);
+			}
+			if (bean instanceof AbstractPreAuthenticatedProcessingFilter filter) {
+				filter.setMfaEnabled(true);
+			}
+			if (bean instanceof BasicAuthenticationFilter filter) {
+				filter.setMfaEnabled(true);
+			}
+			return bean;
+		}
+
+	}
+
+}

+ 50 - 0
config/src/main/java/org/springframework/security/config/annotation/authorization/GlobalMultiFactorAuthenticationSelector.java

@@ -0,0 +1,50 @@
+/*
+ * 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.config.annotation.authorization;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
+
+/**
+ * Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
+ * {@link DefaultAuthorizationManagerFactory}.
+ *
+ * @author Rob Winch
+ * @since 7.0
+ * @see EnableGlobalMultiFactorAuthentication
+ */
+class GlobalMultiFactorAuthenticationSelector implements ImportSelector {
+
+	@Override
+	public String[] selectImports(AnnotationMetadata metadata) {
+		Map<String, Object> multiFactorAuthenticationAttrs = metadata
+			.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
+		String[] authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
+		List<String> imports = new ArrayList<>(2);
+		if (authorities.length > 0) {
+			imports.add(AuthorizationManagerFactoryConfiguration.class.getName());
+		}
+		imports.add(EnableMfaFiltersConfiguration.class.getName());
+		return imports.toArray(new String[imports.size()]);
+	}
+
+}

+ 158 - 0
config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationFiltersSetTests.java

@@ -0,0 +1,158 @@
+/*
+ * 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.config.annotation.authorization;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.http.HttpServletRequest;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockServletContext;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.FactorGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AnyRequestMatcher;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link EnableGlobalMultiFactorAuthentication}.
+ *
+ * @author Rob Winch
+ */
+@ExtendWith(SpringExtension.class)
+@WebAppConfiguration
+@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
+public class EnableGlobalMultiFactorAuthenticationFiltersSetTests {
+
+	@Autowired
+	private AuthenticationManager manager;
+
+	private TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("user", "password", "ROLE_USER",
+			FactorGrantedAuthority.OTT_AUTHORITY);
+
+	@Test
+	void preAuthenticationFilter(@Autowired AbstractAuthenticationProcessingFilter filter) throws Exception {
+		assertMfaEnabled(filter);
+	}
+
+	@Test
+	void authenticationFilter(@Autowired AuthenticationFilter filter) throws Exception {
+		assertMfaEnabled(filter);
+	}
+
+	@Test
+	void preAuthnFilter(@Autowired AbstractPreAuthenticatedProcessingFilter filter) throws Exception {
+		assertMfaEnabled(filter);
+	}
+
+	@Test
+	void basicAuthnFilter(@Autowired BasicAuthenticationFilter filter) throws Exception {
+		assertMfaEnabled(filter);
+	}
+
+	private void assertMfaEnabled(Filter filter) throws Exception {
+		given(this.manager.authenticate(any())).willReturn(this.newAuthn);
+		MockHttpServletRequest request = MockMvcRequestBuilders.get("/")
+			.headers((headers) -> headers.setBasicAuth("u", "p"))
+			.buildRequest(new MockServletContext());
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		filter.doFilter(request, response, chain);
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		assertThat(authentication).isNotNull();
+		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
+			.containsExactlyInAnyOrder(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY,
+					"ROLE_USER");
+	}
+
+	@EnableWebSecurity
+	@Configuration
+	@EnableGlobalMultiFactorAuthentication(
+			authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
+	static class Config {
+
+		@Bean
+		AuthenticationManager authenticationManager() {
+			return mock(AuthenticationManager.class);
+		}
+
+		@Bean
+		static AbstractAuthenticationProcessingFilter authnProcessingFilter(
+				AuthenticationManager authenticationManager) {
+			AbstractAuthenticationProcessingFilter result = new AbstractAuthenticationProcessingFilter(
+					AnyRequestMatcher.INSTANCE, authenticationManager) {
+			};
+			result.setAuthenticationConverter(new BasicAuthenticationConverter());
+			return result;
+		}
+
+		@Bean
+		static AuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) {
+			return new AuthenticationFilter(authenticationManager, new BasicAuthenticationConverter());
+		}
+
+		@Bean
+		static AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter(
+				AuthenticationManager authenticationManager) {
+			AbstractPreAuthenticatedProcessingFilter result = new AbstractPreAuthenticatedProcessingFilter() {
+				@Override
+				protected @Nullable Object getPreAuthenticatedCredentials(HttpServletRequest request) {
+					return "password";
+				}
+
+				@Override
+				protected @Nullable Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
+					return "user";
+				}
+			};
+			result.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
+			result.setAuthenticationManager(authenticationManager);
+			return result;
+		}
+
+		@Bean
+		static BasicAuthenticationFilter basicAuthenticationFilter(AuthenticationManager authenticationManager) {
+			return new BasicAuthenticationFilter(authenticationManager);
+		}
+
+	}
+
+}

+ 54 - 0
config/src/test/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthenticationTests.java

@@ -16,6 +16,13 @@
 
 
 package org.springframework.security.config.annotation.authorization;
 package org.springframework.security.config.annotation.authorization;
 
 
+import java.io.IOException;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,10 +32,18 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.core.authority.FactorGrantedAuthority;
 import org.springframework.security.core.authority.FactorGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MockMvc;
@@ -37,6 +52,8 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.context.WebApplicationContext;
 import org.springframework.web.context.WebApplicationContext;
 
 
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
 import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
 import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -50,12 +67,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @WebAppConfiguration
 @WebAppConfiguration
 public class EnableGlobalMultiFactorAuthenticationTests {
 public class EnableGlobalMultiFactorAuthenticationTests {
 
 
+	private static final String ATTR_NAME = "org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors$SecurityContextRequestPostProcessorSupport$TestSecurityContextRepository.REPO";
+
 	@Autowired
 	@Autowired
 	MockMvc mvc;
 	MockMvc mvc;
 
 
 	@Autowired
 	@Autowired
 	Service service;
 	Service service;
 
 
+	@Test
+	@WithMockUser(authorities = { "ROLE_USER", FactorGrantedAuthority.OTT_AUTHORITY })
+	public void formLoginWhenAuthenticatedThenMergedAuthorities() throws Exception {
+		this.mvc.perform(formLogin())
+			.andExpect(authenticated().withAuthorities("ROLE_USER", FactorGrantedAuthority.OTT_AUTHORITY,
+					FactorGrantedAuthority.PASSWORD_AUTHORITY));
+	}
+
 	@Test
 	@Test
 	@WithMockUser(authorities = { FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY })
 	@WithMockUser(authorities = { FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY })
 	void webWhenAuthorized() throws Exception {
 	void webWhenAuthorized() throws Exception {
@@ -98,6 +125,33 @@ public class EnableGlobalMultiFactorAuthenticationTests {
 			return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
 			return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
 		}
 		}
 
 
+		@Bean
+		static Customizer<HttpSecurity> captureAuthn() {
+			return (http) -> http.addFilterAfter(new Filter() {
+				@Override
+				public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
+						FilterChain filterChain) throws IOException, ServletException {
+					try {
+						filterChain.doFilter(servletRequest, servletResponse);
+					}
+					finally {
+						servletRequest.setAttribute(ATTR_NAME, SecurityContextHolder.getContext());
+					}
+				}
+			}, SecurityContextHolderFilter.class);
+		}
+
+		@Bean
+		@SuppressWarnings("deprecation")
+		UserDetailsService userDetailsService() {
+			UserDetails user = User.withDefaultPasswordEncoder()
+				.username("user")
+				.password("password")
+				.roles("USER")
+				.build();
+			return new InMemoryUserDetailsManager(user);
+		}
+
 		@RestController
 		@RestController
 		static class OkController {
 		static class OkController {
 
 

+ 14 - 0
web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

@@ -158,6 +158,8 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 
 
 	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
 	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
 
 
+	private boolean mfaEnabled;
+
 	/**
 	/**
 	 * @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
 	 * @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
 	 */
 	 */
@@ -289,6 +291,9 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 
 
 	@Contract("null, _ -> false")
 	@Contract("null, _ -> false")
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
+		if (!this.mfaEnabled) {
+			return false;
+		}
 		if (current == null || !current.isAuthenticated()) {
 		if (current == null || !current.isAuthenticated()) {
 			return false;
 			return false;
 		}
 		}
@@ -491,6 +496,15 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 		this.allowSessionCreation = allowSessionCreation;
 		this.allowSessionCreation = allowSessionCreation;
 	}
 	}
 
 
+	/**
+	 * Enables Multi-Factor Authentication (MFA) support.
+	 * @param mfaEnabled true to enable MFA support, false to disable it. Default is
+	 * false.
+	 */
+	public void setMfaEnabled(boolean mfaEnabled) {
+		this.mfaEnabled = mfaEnabled;
+	}
+
 	/**
 	/**
 	 * The session handling strategy which will be invoked immediately after an
 	 * The session handling strategy which will be invoked immediately after an
 	 * authentication request is successfully processed by the
 	 * authentication request is successfully processed by the

+ 14 - 0
web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java

@@ -91,6 +91,8 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 
 
 	private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
 	private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
 
 
+	private boolean mfaEnabled;
+
 	public AuthenticationFilter(AuthenticationManager authenticationManager,
 	public AuthenticationFilter(AuthenticationManager authenticationManager,
 			AuthenticationConverter authenticationConverter) {
 			AuthenticationConverter authenticationConverter) {
 		this((AuthenticationManagerResolver<HttpServletRequest>) (r) -> authenticationManager, authenticationConverter);
 		this((AuthenticationManagerResolver<HttpServletRequest>) (r) -> authenticationManager, authenticationConverter);
@@ -117,6 +119,15 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 		return this.authenticationConverter;
 		return this.authenticationConverter;
 	}
 	}
 
 
+	/**
+	 * Enables Multi-Factor Authentication (MFA) support.
+	 * @param mfaEnabled true to enable MFA support, false to disable it. Default is
+	 * false.
+	 */
+	public void setMfaEnabled(boolean mfaEnabled) {
+		this.mfaEnabled = mfaEnabled;
+	}
+
 	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
 	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
 		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
 		this.authenticationConverter = authenticationConverter;
 		this.authenticationConverter = authenticationConverter;
@@ -219,6 +230,9 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 
 
 	@Contract("null, _ -> false")
 	@Contract("null, _ -> false")
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
+		if (!this.mfaEnabled) {
+			return false;
+		}
 		if (current == null || !current.isAuthenticated()) {
 		if (current == null || !current.isAuthenticated()) {
 			return false;
 			return false;
 		}
 		}

+ 14 - 0
web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

@@ -123,6 +123,8 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 
 
 	private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
 	private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
 
 
+	private boolean mfaEnabled;
+
 	/**
 	/**
 	 * Check whether all required properties have been set.
 	 * Check whether all required properties have been set.
 	 */
 	 */
@@ -238,6 +240,9 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 
 
 	@Contract("null, _ -> false")
 	@Contract("null, _ -> false")
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
+		if (!this.mfaEnabled) {
+			return false;
+		}
 		if (current == null || !current.isAuthenticated()) {
 		if (current == null || !current.isAuthenticated()) {
 			return false;
 			return false;
 		}
 		}
@@ -299,6 +304,15 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 		this.eventPublisher = anApplicationEventPublisher;
 		this.eventPublisher = anApplicationEventPublisher;
 	}
 	}
 
 
+	/**
+	 * Enables Multi-Factor Authentication (MFA) support.
+	 * @param mfaEnabled true to enable MFA support, false to disable it. Default is
+	 * false.
+	 */
+	public void setMfaEnabled(boolean mfaEnabled) {
+		this.mfaEnabled = mfaEnabled;
+	}
+
 	/**
 	/**
 	 * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
 	 * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
 	 * authentication success. The default action is to save the {@link SecurityContext}
 	 * authentication success. The default action is to save the {@link SecurityContext}

+ 14 - 0
web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java

@@ -116,6 +116,8 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 
 
 	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
 	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
 
 
+	private boolean mfaEnabled;
+
 	/**
 	/**
 	 * Creates an instance which will authenticate against the supplied
 	 * Creates an instance which will authenticate against the supplied
 	 * {@code AuthenticationManager} and which will ignore failed authentication attempts,
 	 * {@code AuthenticationManager} and which will ignore failed authentication attempts,
@@ -156,6 +158,15 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 		this.securityContextRepository = securityContextRepository;
 		this.securityContextRepository = securityContextRepository;
 	}
 	}
 
 
+	/**
+	 * Enables Multi-Factor Authentication (MFA) support.
+	 * @param mfaEnabled true to enable MFA support, false to disable it. Default is
+	 * false.
+	 */
+	public void setMfaEnabled(boolean mfaEnabled) {
+		this.mfaEnabled = mfaEnabled;
+	}
+
 	/**
 	/**
 	 * Sets the
 	 * Sets the
 	 * {@link org.springframework.security.web.authentication.AuthenticationConverter} to
 	 * {@link org.springframework.security.web.authentication.AuthenticationConverter} to
@@ -238,6 +249,9 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 
 
 	@Contract("null, _ -> false")
 	@Contract("null, _ -> false")
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
 	private boolean shouldPerformMfa(@Nullable Authentication current, Authentication authenticationResult) {
+		if (!this.mfaEnabled) {
+			return false;
+		}
 		if (current == null || !current.isAuthenticated()) {
 		if (current == null || !current.isAuthenticated()) {
 			return false;
 			return false;
 		}
 		}

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

@@ -457,12 +457,29 @@ public class AbstractAuthenticationProcessingFilterTests {
 		Authentication newAuthn = UsernamePasswordAuthenticationToken.authenticated(existingAuthn.getName(), "test",
 		Authentication newAuthn = UsernamePasswordAuthenticationToken.authenticated(existingAuthn.getName(), "test",
 				AuthorityUtils.createAuthorityList("TEST"));
 				AuthorityUtils.createAuthorityList("TEST"));
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain(false));
 		filter.doFilter(request, response, new MockFilterChain(false));
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 	}
 	}
 
 
+	@Test
+	void doFilterWhenDefaultThenMfaDisabled() throws Exception {
+		String ROLE_EXISTING = "ROLE_EXISTING";
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
+				ROLE_EXISTING);
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		MockHttpServletRequest request = createMockAuthenticationRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		Authentication newAuthn = UsernamePasswordAuthenticationToken.authenticated(existingAuthn.getName(), "test",
+				AuthorityUtils.createAuthorityList("TEST"));
+		MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
+		filter.doFilter(request, response, new MockFilterChain(false));
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		assertThat(authentication).isEqualTo(newAuthn);
+	}
+
 	// gh-18112
 	// gh-18112
 	@Test
 	@Test
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
@@ -475,6 +492,7 @@ public class AbstractAuthenticationProcessingFilterTests {
 		Authentication newAuthn = UsernamePasswordAuthenticationToken
 		Authentication newAuthn = UsernamePasswordAuthenticationToken
 			.authenticated(existingAuthn.getName() + "different", "test", AuthorityUtils.createAuthorityList("TEST"));
 			.authenticated(existingAuthn.getName() + "different", "test", AuthorityUtils.createAuthorityList("TEST"));
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(newAuthn);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain(false));
 		filter.doFilter(request, response, new MockFilterChain(false));
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication).isEqualTo(newAuthn);
 		assertThat(authentication).isEqualTo(newAuthn);
@@ -494,6 +512,7 @@ public class AbstractAuthenticationProcessingFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(
 				new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
 				new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain(false));
 		filter.doFilter(request, response, new MockFilterChain(false));
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(new ArrayList<GrantedAuthority>(authentication.getAuthorities()))
 		assertThat(new ArrayList<GrantedAuthority>(authentication.getAuthorities()))
@@ -509,6 +528,7 @@ public class AbstractAuthenticationProcessingFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(
 		MockAuthenticationFilter filter = new MockAuthenticationFilter(
 				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
 				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain(false));
 		filter.doFilter(request, response, new MockFilterChain(false));
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		SecurityAssertions.assertThat(authentication)
 		SecurityAssertions.assertThat(authentication)

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

@@ -320,12 +320,33 @@ public class AuthenticationFilterTests {
 		FilterChain chain = new MockFilterChain();
 		FilterChain chain = new MockFilterChain();
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 				this.authenticationConverter);
 				this.authenticationConverter);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, chain);
 		filter.doFilter(request, response, chain);
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 	}
 	}
 
 
+	@Test
+	public void doFilterWhenDefaultThenMfaDisabled() throws Exception {
+		String ROLE_EXISTING = "ROLE_EXISTING";
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
+				ROLE_EXISTING);
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		given(this.authenticationConverter.convert(any())).willReturn(existingAuthn);
+		TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName(), "password",
+				"TEST");
+		given(this.authenticationManager.authenticate(any())).willReturn(newAuthn);
+		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();
+		assertThat(authentication).isEqualTo(newAuthn);
+	}
+
 	// gh-18112
 	// gh-18112
 	@Test
 	@Test
 	public void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
 	public void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
@@ -342,6 +363,7 @@ public class AuthenticationFilterTests {
 		FilterChain chain = new MockFilterChain();
 		FilterChain chain = new MockFilterChain();
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 				this.authenticationConverter);
 				this.authenticationConverter);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, chain);
 		filter.doFilter(request, response, chain);
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication).isEqualTo(expected);
 		assertThat(authentication).isEqualTo(expected);
@@ -365,6 +387,7 @@ public class AuthenticationFilterTests {
 		FilterChain chain = new MockFilterChain();
 		FilterChain chain = new MockFilterChain();
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 				this.authenticationConverter);
 				this.authenticationConverter);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, chain);
 		filter.doFilter(request, response, chain);
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
@@ -383,6 +406,7 @@ public class AuthenticationFilterTests {
 		FilterChain chain = new MockFilterChain();
 		FilterChain chain = new MockFilterChain();
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 		AuthenticationFilter filter = new AuthenticationFilter(this.authenticationManager,
 				this.authenticationConverter);
 				this.authenticationConverter);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, chain);
 		filter.doFilter(request, response, chain);
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		SecurityAssertions.assertThat(authentication)
 		SecurityAssertions.assertThat(authentication)

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

@@ -406,6 +406,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		this.filter = createFilterAuthenticatesWith(new TestingAuthenticationToken("username", "password", "TEST"));
 		this.filter = createFilterAuthenticatesWith(new TestingAuthenticationToken("username", "password", "TEST"));
+		this.filter.setMfaEnabled(true);
 		this.filter.doFilter(request, response, new MockFilterChain());
 		this.filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		// @formatter:off
 		// @formatter:off
@@ -415,6 +416,21 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		// @formatter:on
 		// @formatter:on
 	}
 	}
 
 
+	@Test
+	void doFilterWhenDefaultThenMfaDisabled() throws Exception {
+		String ROLE_EXISTING = "ROLE_EXISTING";
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
+				ROLE_EXISTING);
+		SecurityContextHolder.setContext(new SecurityContextImpl(existingAuthn));
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("username", "password", "TEST");
+		this.filter = createFilterAuthenticatesWith(newAuthn);
+		this.filter.doFilter(request, response, new MockFilterChain());
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		assertThat(authentication).isEqualTo(newAuthn);
+	}
+
 	// gh-18112
 	// gh-18112
 	@Test
 	@Test
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
@@ -427,6 +443,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName() + "different",
 		TestingAuthenticationToken newAuthn = new TestingAuthenticationToken(existingAuthn.getName() + "different",
 				"password", "TEST");
 				"password", "TEST");
 		this.filter = createFilterAuthenticatesWith(newAuthn);
 		this.filter = createFilterAuthenticatesWith(newAuthn);
+		this.filter.setMfaEnabled(true);
 		this.filter.doFilter(request, response, new MockFilterChain());
 		this.filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication).isEqualTo(newAuthn);
 		assertThat(authentication).isEqualTo(newAuthn);
@@ -446,6 +463,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		this.filter = createFilterAuthenticatesWith(
 		this.filter = createFilterAuthenticatesWith(
 				new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
 				new TestingAuthenticationToken("username", "password", new DefaultEqualsGrantedAuthority()));
+		this.filter.setMfaEnabled(true);
 		this.filter.doFilter(request, response, new MockFilterChain());
 		this.filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		// @formatter:off
 		// @formatter:off
@@ -463,6 +481,7 @@ public class AbstractPreAuthenticatedProcessingFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		this.filter = createFilterAuthenticatesWith(
 		this.filter = createFilterAuthenticatesWith(
 				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
 				new NonBuildableAuthenticationToken("username", "password", "FACTORTWO"));
+		this.filter.setMfaEnabled(true);
 		this.filter.doFilter(request, response, new MockFilterChain());
 		this.filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		SecurityAssertions.assertThat(authentication)
 		SecurityAssertions.assertThat(authentication)

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

@@ -511,12 +511,31 @@ public class BasicAuthenticationFilterTests {
 		AuthenticationManager manager = mock(AuthenticationManager.class);
 		AuthenticationManager manager = mock(AuthenticationManager.class);
 		given(manager.authenticate(any())).willReturn(new TestingAuthenticationToken("username", "password", "TEST"));
 		given(manager.authenticate(any())).willReturn(new TestingAuthenticationToken("username", "password", "TEST"));
 		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
 		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain());
 		filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 		assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 			.containsExactlyInAnyOrder(ROLE_EXISTING, "TEST");
 	}
 	}
 
 
+	@Test
+	void doFilterWhenDefaultThenMfaDisabled() throws Exception {
+		String ROLE_EXISTING = "ROLE_EXISTING";
+		TestingAuthenticationToken existingAuthn = new TestingAuthenticationToken("username", "password",
+				ROLE_EXISTING);
+		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);
+		TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("username", "password", "TEST");
+		given(manager.authenticate(any())).willReturn(newAuthn);
+		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
+		filter.doFilter(request, response, new MockFilterChain());
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		assertThat(authentication).isEqualTo(newAuthn);
+	}
+
 	// gh-18112
 	// gh-18112
 	@Test
 	@Test
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
 	void doFilterWhenDifferentPrincipalThenDoesNotCombine() throws Exception {
@@ -532,6 +551,7 @@ public class BasicAuthenticationFilterTests {
 				"password", "TEST");
 				"password", "TEST");
 		given(manager.authenticate(any())).willReturn(newAuthn);
 		given(manager.authenticate(any())).willReturn(newAuthn);
 		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
 		BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager);
+		filter.setMfaEnabled(true);
 		filter.doFilter(request, response, new MockFilterChain());
 		filter.doFilter(request, response, new MockFilterChain());
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 		assertThat(authentication).isEqualTo(newAuthn);
 		assertThat(authentication).isEqualTo(newAuthn);