Skip to content

Commit f652920

Browse files
committed
Add @EnableGlobalMultiFactorAuthentication
Closes gh-17954
1 parent e33e4d8 commit f652920

File tree

9 files changed

+660
-0
lines changed

9 files changed

+660
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.context.annotation.Import;
26+
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
27+
28+
/**
29+
* Exposes a {@link DefaultAuthorizationManagerFactory} as a Bean with the
30+
* {@link #authorities()} specified as additional required authorities. The configuration
31+
* will be picked up by both
32+
* {@link org.springframework.security.config.annotation.web.configuration.EnableWebSecurity}
33+
* and
34+
* {@link org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity}.
35+
*
36+
* <pre>
37+
38+
* &#64;Configuration
39+
* &#64;EnableGlobalMultiFactorAuthentication(authorities = { GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
40+
* public class MyConfiguration {
41+
* // ...
42+
* }
43+
* </pre>
44+
*
45+
* NOTE: At this time reactive applications do not support MFA and thus are not impacted.
46+
* This will likely be enhanced in the future.
47+
*
48+
* @author Rob Winch
49+
* @since 7.0
50+
*/
51+
@Retention(RetentionPolicy.RUNTIME)
52+
@Target(ElementType.TYPE)
53+
@Documented
54+
@Import(GlobalMultiFactorAuthenticationConfiguration.class)
55+
public @interface EnableGlobalMultiFactorAuthentication {
56+
57+
/**
58+
* The additional authorities that are required.
59+
* @return the additional authorities that are required (e.g. {
60+
* GrantedAuthorities.FACTOR_OTT, GrantedAuthorities.FACTOR_PASSWORD })
61+
* @see org.springframework.security.core.GrantedAuthorities
62+
*/
63+
String[] authorities();
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.Map;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.ImportAware;
24+
import org.springframework.core.type.AnnotationMetadata;
25+
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
26+
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
27+
28+
/**
29+
* Uses {@link EnableGlobalMultiFactorAuthentication} to configure a
30+
* {@link DefaultAuthorizationManagerFactory}.
31+
*
32+
* @author Rob Winch
33+
* @since 7.0
34+
* @see EnableGlobalMultiFactorAuthentication
35+
*/
36+
class GlobalMultiFactorAuthenticationConfiguration implements ImportAware {
37+
38+
private String[] authorities;
39+
40+
@Bean
41+
DefaultAuthorizationManagerFactory authorizationManagerFactory(ObjectProvider<RoleHierarchy> roleHierarchy) {
42+
DefaultAuthorizationManagerFactory.Builder<Object> builder = DefaultAuthorizationManagerFactory.builder()
43+
.requireAdditionalAuthorities(this.authorities);
44+
roleHierarchy.ifAvailable(builder::roleHierarchy);
45+
return builder.build();
46+
}
47+
48+
@Override
49+
public void setImportMetadata(AnnotationMetadata importMetadata) {
50+
Map<String, Object> multiFactorAuthenticationAttrs = importMetadata
51+
.getAnnotationAttributes(EnableGlobalMultiFactorAuthentication.class.getName());
52+
53+
this.authorities = (String[]) multiFactorAuthenticationAttrs.get("authorities");
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 org.assertj.core.api.Assertions;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.ExtendWith;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.security.access.AccessDeniedException;
27+
import org.springframework.security.access.prepost.PreAuthorize;
28+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
29+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
30+
import org.springframework.security.core.GrantedAuthorities;
31+
import org.springframework.security.test.context.support.WithMockUser;
32+
import org.springframework.test.context.junit.jupiter.SpringExtension;
33+
import org.springframework.test.context.web.WebAppConfiguration;
34+
import org.springframework.test.web.servlet.MockMvc;
35+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
36+
import org.springframework.web.bind.annotation.GetMapping;
37+
import org.springframework.web.bind.annotation.RestController;
38+
import org.springframework.web.context.WebApplicationContext;
39+
40+
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
41+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
42+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
43+
44+
/**
45+
* Tests for {@link EnableGlobalMultiFactorAuthentication}.
46+
*
47+
* @author Rob Winch
48+
*/
49+
@ExtendWith(SpringExtension.class)
50+
@WebAppConfiguration
51+
public class EnableGlobalMultiFactorAuthenticationTests {
52+
53+
@Autowired
54+
MockMvc mvc;
55+
56+
@Autowired
57+
Service service;
58+
59+
@Test
60+
@WithMockUser(
61+
authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
62+
void webWhenAuthorized() throws Exception {
63+
this.mvc.perform(get("/")).andExpect(status().isOk());
64+
}
65+
66+
@Test
67+
@WithMockUser
68+
void webWhenNotAuthorized() throws Exception {
69+
this.mvc.perform(get("/")).andExpect(status().isUnauthorized());
70+
}
71+
72+
@Test
73+
@WithMockUser(
74+
authorities = { GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY, GrantedAuthorities.FACTOR_OTT_AUTHORITY })
75+
void methodWhenAuthorized() throws Exception {
76+
Assertions.assertThatNoException().isThrownBy(() -> this.service.authenticated());
77+
}
78+
79+
@Test
80+
@WithMockUser
81+
void methodWhenNotAuthorized() throws Exception {
82+
Assertions.assertThatExceptionOfType(AccessDeniedException.class)
83+
.isThrownBy(() -> this.service.authenticated());
84+
}
85+
86+
@EnableWebSecurity
87+
@EnableMethodSecurity
88+
@Configuration
89+
@EnableGlobalMultiFactorAuthentication(
90+
authorities = { GrantedAuthorities.FACTOR_OTT_AUTHORITY, GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY })
91+
static class Config {
92+
93+
@Bean
94+
Service service() {
95+
return new Service();
96+
}
97+
98+
@Bean
99+
MockMvc mvc(WebApplicationContext context) {
100+
return MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
101+
}
102+
103+
@RestController
104+
static class OkController {
105+
106+
@GetMapping("/")
107+
String ok() {
108+
return "ok";
109+
}
110+
111+
}
112+
113+
}
114+
115+
static class Service {
116+
117+
@PreAuthorize("isAuthenticated()")
118+
void authenticated() {
119+
}
120+
121+
}
122+
123+
}

docs/modules/ROOT/pages/servlet/authentication/adaptive.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ This yields a more familiar configuration:
6262

6363
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=httpSecurity,indent=0]
6464

65+
[[enable-global-mfa]]
66+
=== @EnableGlobalMultiFactorAuthentication
67+
68+
You can simplify the configuration even further by using `@EnableGlobalMultiFactorAuthentication` to create the `AuthorizationManagerFactory` for you.
69+
70+
include-code::./EnableGlobalMultiFactorAuthenticationConfiguration[tag=enable-global-mfa,indent=0]
71+
72+
6573
[[obtaining-more-authorization]]
6674
== Authorizing More Scopes
6775

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.springframework.security.docs.servlet.authentication.enableglobalmfa;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.config.Customizer;
6+
import org.springframework.security.config.annotation.authorization.EnableGlobalMultiFactorAuthentication;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9+
import org.springframework.security.core.GrantedAuthorities;
10+
import org.springframework.security.core.userdetails.User;
11+
import org.springframework.security.core.userdetails.UserDetailsService;
12+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
13+
import org.springframework.security.web.SecurityFilterChain;
14+
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
15+
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
16+
17+
@EnableWebSecurity
18+
@Configuration(proxyBeanMethods = false)
19+
// tag::enable-global-mfa[]
20+
@EnableGlobalMultiFactorAuthentication(authorities = {
21+
GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
22+
GrantedAuthorities.FACTOR_OTT_AUTHORITY })
23+
// end::enable-global-mfa[]
24+
public class EnableGlobalMultiFactorAuthenticationConfiguration {
25+
26+
// tag::httpSecurity[]
27+
@Bean
28+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
29+
// @formatter:off
30+
http
31+
.authorizeHttpRequests((authorize) -> authorize
32+
.requestMatchers("/admin/**").hasRole("ADMIN")
33+
.anyRequest().authenticated()
34+
)
35+
.formLogin(Customizer.withDefaults())
36+
.oneTimeTokenLogin(Customizer.withDefaults());
37+
// @formatter:on
38+
return http.build();
39+
}
40+
// end::httpSecurity[]
41+
42+
@Bean
43+
UserDetailsService userDetailsService() {
44+
return new InMemoryUserDetailsManager(
45+
User.withDefaultPasswordEncoder()
46+
.username("user")
47+
.password("password")
48+
.authorities("app")
49+
.build()
50+
);
51+
}
52+
53+
@Bean
54+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
55+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
56+
}
57+
}
58+

0 commit comments

Comments
 (0)