Skip to content

Commit aaf738f

Browse files
committed
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
1 parent ccd39a2 commit aaf738f

File tree

14 files changed

+472
-4
lines changed

14 files changed

+472
-4
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
* @since 7.0
3535
* @see EnableGlobalMultiFactorAuthentication
3636
*/
37-
class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
37+
class AuthorizationManagerFactoryConfiguration implements ImportAware {
3838

3939
private String[] authorities;
4040

@@ -51,7 +51,7 @@ public void setImportMetadata(AnnotationMetadata importMetadata) {
5151
Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
5252
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
5353

54-
this.authorities = (String[]) multiFactorAuthenticationAttrs.get("authorities");
54+
this.authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
5555
}
5656

5757
}

config/src/main/java/org/springframework/security/config/annotation/authorization/EnableGlobalMultiFactorAuthentication.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@
5151
@Retention(RetentionPolicy.RUNTIME)
5252
@Target(ElementType.TYPE)
5353
@Documented
54-
@Import(GlobalMultiFactorAuthenticationConfiguration.class)
54+
@Import(GlobalMultiFactorAuthenticationSelector.class)
5555
public @interface EnableGlobalMultiFactorAuthentication {
5656

5757
/**
5858
* The additional authorities that are required.
5959
* @return the additional authorities that are required (e.g. {
60-
* FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD })
60+
* FactorGrantedAuthority.FACTOR_OTT, FactorGrantedAuthority.FACTOR_PASSWORD }). Can
61+
* be null or an empty array if no additional authorities are required (if
62+
* authorization rules are not globally requiring MFA).
6163
* @see org.springframework.security.core.authority.FactorGrantedAuthority
6264
*/
6365
String[] authorities();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.authorization;
18+
19+
import org.jspecify.annotations.Nullable;
20+
21+
import org.springframework.beans.BeansException;
22+
import org.springframework.beans.factory.config.BeanPostProcessor;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
26+
import org.springframework.security.web.authentication.AuthenticationFilter;
27+
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
28+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
29+
30+
@Configuration(proxyBeanMethods = false)
31+
class EnableMfaFiltersConfiguration {
32+
33+
@Bean
34+
BeanPostProcessor mfaBeanPostProcessor() {
35+
return new EnableMfaFiltersPostProcessor();
36+
}
37+
38+
/**
39+
* A {@link BeanPostProcessor} that enables MFA on authentication filters.
40+
*
41+
* @author Rob Winch
42+
* @since 7.0
43+
*/
44+
private static class EnableMfaFiltersPostProcessor implements BeanPostProcessor {
45+
46+
@Override
47+
public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
48+
if (bean instanceof AbstractAuthenticationProcessingFilter filter) {
49+
filter.setMfaEnabled(true);
50+
}
51+
if (bean instanceof AuthenticationFilter filter) {
52+
filter.setMfaEnabled(true);
53+
}
54+
if (bean instanceof AbstractPreAuthenticatedProcessingFilter filter) {
55+
filter.setMfaEnabled(true);
56+
}
57+
if (bean instanceof BasicAuthenticationFilter filter) {
58+
filter.setMfaEnabled(true);
59+
}
60+
return bean;
61+
}
62+
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.authorization;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.springframework.context.annotation.ImportSelector;
24+
import org.springframework.core.type.AnnotationMetadata;
25+
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
26+
27+
/**
28+
* Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
29+
* {@link DefaultAuthorizationManagerFactory}.
30+
*
31+
* @author Rob Winch
32+
* @since 7.0
33+
* @see EnableGlobalMultiFactorAuthentication
34+
*/
35+
class GlobalMultiFactorAuthenticationSelector implements ImportSelector {
36+
37+
@Override
38+
public String[] selectImports(AnnotationMetadata metadata) {
39+
Map<String, Object> multiFactorAuthenticationAttrs = metadata
40+
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
41+
String[] authorities = (String[]) multiFactorAuthenticationAttrs.getOrDefault("authorities", new String[0]);
42+
List<String> imports = new ArrayList<>(2);
43+
if (authorities.length > 0) {
44+
imports.add(AuthorizationManagerFactoryConfiguration.class.getName());
45+
}
46+
imports.add(EnableMfaFiltersConfiguration.class.getName());
47+
return imports.toArray(new String[imports.size()]);
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.authorization;
18+
19+
import jakarta.servlet.Filter;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import org.jspecify.annotations.Nullable;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.mock.web.MockFilterChain;
29+
import org.springframework.mock.web.MockHttpServletRequest;
30+
import org.springframework.mock.web.MockHttpServletResponse;
31+
import org.springframework.mock.web.MockServletContext;
32+
import org.springframework.security.authentication.AuthenticationManager;
33+
import org.springframework.security.authentication.TestingAuthenticationToken;
34+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
35+
import org.springframework.security.core.Authentication;
36+
import org.springframework.security.core.GrantedAuthority;
37+
import org.springframework.security.core.authority.FactorGrantedAuthority;
38+
import org.springframework.security.core.context.SecurityContextHolder;
39+
import org.springframework.security.test.context.support.WithMockUser;
40+
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
41+
import org.springframework.security.web.authentication.AuthenticationFilter;
42+
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
43+
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
44+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
45+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
46+
import org.springframework.test.context.junit.jupiter.SpringExtension;
47+
import org.springframework.test.context.web.WebAppConfiguration;
48+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
49+
50+
import static org.assertj.core.api.Assertions.assertThat;
51+
import static org.mockito.ArgumentMatchers.any;
52+
import static org.mockito.BDDMockito.given;
53+
import static org.mockito.Mockito.mock;
54+
55+
/**
56+
* Tests for {@link EnableGlobalMultiFactorAuthentication}.
57+
*
58+
* @author Rob Winch
59+
*/
60+
@ExtendWith(SpringExtension.class)
61+
@WebAppConfiguration
62+
@WithMockUser(authorities = FactorGrantedAuthority.PASSWORD_AUTHORITY)
63+
public class EnableGlobalMultiFactorAuthenticationFiltersSetTests {
64+
65+
@Autowired
66+
private AuthenticationManager manager;
67+
68+
private TestingAuthenticationToken newAuthn = new TestingAuthenticationToken("user", "password", "ROLE_USER",
69+
FactorGrantedAuthority.OTT_AUTHORITY);
70+
71+
@Test
72+
void preAuthenticationFilter(@Autowired AbstractAuthenticationProcessingFilter filter) throws Exception {
73+
assertMfaEnabled(filter);
74+
}
75+
76+
@Test
77+
void authenticationFilter(@Autowired AuthenticationFilter filter) throws Exception {
78+
assertMfaEnabled(filter);
79+
}
80+
81+
@Test
82+
void preAuthnFilter(@Autowired AbstractPreAuthenticatedProcessingFilter filter) throws Exception {
83+
assertMfaEnabled(filter);
84+
}
85+
86+
@Test
87+
void basicAuthnFilter(@Autowired BasicAuthenticationFilter filter) throws Exception {
88+
assertMfaEnabled(filter);
89+
}
90+
91+
private void assertMfaEnabled(Filter filter) throws Exception {
92+
given(this.manager.authenticate(any())).willReturn(this.newAuthn);
93+
MockHttpServletRequest request = MockMvcRequestBuilders.get("/")
94+
.headers((headers) -> headers.setBasicAuth("u", "p"))
95+
.buildRequest(new MockServletContext());
96+
MockHttpServletResponse response = new MockHttpServletResponse();
97+
MockFilterChain chain = new MockFilterChain();
98+
filter.doFilter(request, response, chain);
99+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
100+
assertThat(authentication).isNotNull();
101+
assertThat(authentication.getAuthorities()).extracting(GrantedAuthority::getAuthority)
102+
.containsExactlyInAnyOrder(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY,
103+
"ROLE_USER");
104+
}
105+
106+
@EnableWebSecurity
107+
@Configuration
108+
@EnableGlobalMultiFactorAuthentication(
109+
authorities = { FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY })
110+
static class Config {
111+
112+
@Bean
113+
AuthenticationManager authenticationManager() {
114+
return mock(AuthenticationManager.class);
115+
}
116+
117+
@Bean
118+
static AbstractAuthenticationProcessingFilter authnProcessingFilter(
119+
AuthenticationManager authenticationManager) {
120+
AbstractAuthenticationProcessingFilter result = new AbstractAuthenticationProcessingFilter(
121+
AnyRequestMatcher.INSTANCE, authenticationManager) {
122+
};
123+
result.setAuthenticationConverter(new BasicAuthenticationConverter());
124+
return result;
125+
}
126+
127+
@Bean
128+
static AuthenticationFilter authenticationFilter(AuthenticationManager authenticationManager) {
129+
return new AuthenticationFilter(authenticationManager, new BasicAuthenticationConverter());
130+
}
131+
132+
@Bean
133+
static AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter(
134+
AuthenticationManager authenticationManager) {
135+
AbstractPreAuthenticatedProcessingFilter result = new AbstractPreAuthenticatedProcessingFilter() {
136+
@Override
137+
protected @Nullable Object getPreAuthenticatedCredentials(HttpServletRequest request) {
138+
return "password";
139+
}
140+
141+
@Override
142+
protected @Nullable Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
143+
return "user";
144+
}
145+
};
146+
result.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
147+
result.setAuthenticationManager(authenticationManager);
148+
return result;
149+
}
150+
151+
@Bean
152+
static BasicAuthenticationFilter basicAuthenticationFilter(AuthenticationManager authenticationManager) {
153+
return new BasicAuthenticationFilter(authenticationManager);
154+
}
155+
156+
}
157+
158+
}

0 commit comments

Comments
 (0)