Skip to content

Commit bbba293

Browse files
committed
Add Initial Documentation
Issue spring-projectsgh-17934
1 parent d757e6e commit bbba293

29 files changed

+2224
-0
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
***** xref:servlet/authentication/passwords/password-encoder.adoc[PasswordEncoder]
5050
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
5151
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
52+
*** xref:servlet/authentication/adaptive.adoc[Multifactor Authentication]
5253
*** xref:servlet/authentication/persistence.adoc[Persistence]
5354
*** xref:servlet/authentication/passkeys.adoc[Passkeys]
5455
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
= Adaptive Authentication
2+
3+
Since authentication needs can vary from person-to-person and even from one login attempt to the next, Spring Security supports adapting authentication requirements to each situation.
4+
5+
Some of the most common applications of this principal are:
6+
7+
1. *Re-authentication* - Users need to provide authentication again in order to enter an area of elevated security
8+
2. *Multi-factor Authentication* - Users need more than one authentication mechanism to pass in order to access secured resources
9+
3. *Authorizing More Scopes* - Users are allowed to consent to a subset of scopes from an OAuth 2.0 Authorization Server.
10+
Then, if later on a scope that they did not grant is needed, consent can be re-requested for just that scope.
11+
4. *Opting-in to Stronger Authentication Mechanisms* - Users may not be ready yet to start using MFA, but the application wants to allow the subset of security-minded users to opt-in.
12+
5. *Requiring Additional Steps for Suspicious Logins* - The application may notice that the user's IP address has changed, that they are behind a VPN, or some other consideration that requires additional verification
13+
14+
[[re-authentication]]
15+
== Re-authentication
16+
17+
The most common of these is re-authentication.
18+
Imagine an application configured in the following way:
19+
20+
include-code::./SimpleConfiguration[tag=httpSecurity,indent=0]
21+
22+
By default, this application has two authentication mechanisms that it allows, meaning that the user could use either one and be fully-authenticated.
23+
24+
If there is a set of endpoints that require a specific factor, we can specify that in `authorizeHttpRequests` as follows:
25+
26+
include-code::./RequireOttConfiguration[tag=httpSecurity,indent=0]
27+
<1> - States that all `/profile/**` endpoints require one-time-token login to be authorized
28+
29+
Given the above configuration, users can log in with any mechanism that you support.
30+
And, if they want to visit the profile page, then Spring Security will redirect them to the One-Time-Token Login page to obtain it.
31+
32+
In this way, the authority given to a user is directly proportional to the amount of proof given.
33+
This adaptive approach allows users to give only the proof needed to perform their intended operations.
34+
35+
[[multi-factor-authentication]]
36+
== Multi-Factor Authentication
37+
38+
You may require that all users require both One-Time-Token login and Username/Password login to access any part of your site.
39+
40+
To require both, you can state an authorization rule with `anyRequest` like so:
41+
42+
include-code::./ListAuthoritiesConfiguration[tag=httpSecurity,indent=0]
43+
<1> - This states that both `FACTOR_PASSWORD` and `FACTOR_OTT` are needed to use any part of the application
44+
45+
Spring Security behind the scenes knows which endpoint to go to depending on which authority is missing.
46+
If the user logged in initially with their username and password, then Spring Security redirects to the One-Time-Token Login page.
47+
If the user logged in initially with a token, then Spring Security redirects to the Username/Password Login page.
48+
49+
[[authorization-manager-factory]]
50+
=== Requiring MFA For All Endpoints
51+
52+
Specifying all authorities for each request pattern could be unwanted boilerplate:
53+
54+
include-code::./ListAuthoritiesEverywhereConfiguration[tag=httpSecurity,indent=0]
55+
<1> - Since all authorities need to be specified for each endpoint, deploying MFA in this way can create unwanted boilerplate
56+
57+
This can be remedied by publishing an `AuthorizationManagerFactory` bean like so:
58+
59+
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=authorizationManagerFactoryBean,indent=0]
60+
61+
This yields a more familiar configuration:
62+
63+
include-code::./UseAuthorizationManagerFactoryConfiguration[tag=httpSecurity,indent=0]
64+
65+
[[obtaining-more-authorization]]
66+
== Authorizing More Scopes
67+
68+
You can also configure exception handling to direct Spring Security on how to obtain a missing scope.
69+
70+
Consider an application that requires a specific OAuth 2.0 scope for a given endpoint:
71+
72+
include-code::./ScopeConfiguration[tag=httpSecurity,indent=0]
73+
74+
If this is also configured with an `AuthorizationManagerFactory` bean like this one:
75+
76+
include-code::./MissingAuthorityConfiguration[tag=authorizationManagerFactoryBean,indent=0]
77+
78+
Then the application will require an X.509 certificate as well as authorization from an OAuth 2.0 authorization server.
79+
80+
In the event that the user does not consent to `profile:read`, this application as it stands will issue a 403.
81+
However, if you have a way for the application to re-ask for consent, then you can implement this in an `AuthenticationEntryPoint` like the following:
82+
83+
include-code::./MissingAuthorityConfiguration[tag=authenticationEntryPoint,indent=0]
84+
85+
Then, your filter chain declaration can bind this entry point to the given authority like so:
86+
87+
include-code::./MissingAuthorityConfiguration[tag=httpSecurity,indent=0]
88+
89+
[[custom-authorization-manager-factory]]
90+
== Programmatically Decide Which Authorities Are Required
91+
92+
`AuthorizationManager` is the core interface for making authorization decisions.
93+
Consider an authorization manager that looks at the logged in user to decide which factors are necessary:
94+
95+
include-code::./CustomAuthorizationManagerFactory[tag=authorizationManager,indent=0]
96+
97+
In this case, using One-Time-Token is only required for those who have opted in.
98+
99+
This can then be enforced by a custom `AuthorizationManagerFactory` implementation:
100+
101+
include-code::./CustomAuthorizationManagerFactory[tag=authorizationManagerFactory,indent=0]

docs/modules/ROOT/pages/whats-new.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Each section that follows will indicate the more notable removals as well as the
1515

1616
== Core
1717

18+
* Added Support for xref:servlet/authentication/adaptive.adoc[Multi-factor Authentication]
1819
* Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
1920
* Added javadoc:org.springframework.security.authorization.AllAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.AllAuthoritiesReactiveAuthorizationManager[] along with corresponding methods for xref:servlet/authorization/authorize-http-requests.adoc#authorize-requests[Authorizing `HttpServletRequests`] and xref:servlet/authorization/method-security.adoc#using-authorization-expression-fields-and-methods[method security expressions].
2021
* Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.docs.servlet.authentication.authorizationmanagerfactory;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.security.config.test.SpringTestContext;
24+
import org.springframework.security.config.test.SpringTestContextExtension;
25+
import org.springframework.security.docs.servlet.authentication.servletx509config.CustomX509Configuration;
26+
import org.springframework.security.test.context.support.WithMockUser;
27+
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
28+
import org.springframework.test.context.TestExecutionListeners;
29+
import org.springframework.test.context.junit.jupiter.SpringExtension;
30+
import org.springframework.test.web.servlet.MockMvc;
31+
import org.springframework.web.bind.annotation.GetMapping;
32+
import org.springframework.web.bind.annotation.RestController;
33+
34+
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
35+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
36+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
37+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
38+
39+
/**
40+
* Tests {@link CustomX509Configuration}.
41+
*
42+
* @author Rob Winch
43+
*/
44+
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
45+
@TestExecutionListeners(WithSecurityContextTestExecutionListener.class)
46+
public class AuthorizationManagerFactoryTests {
47+
48+
public final SpringTestContext spring = new SpringTestContext(this);
49+
50+
@Autowired
51+
MockMvc mockMvc;
52+
53+
@Test
54+
@WithMockUser(authorities = { "FACTOR_PASSWORD", "FACTOR_OTT" })
55+
void getWhenAuthenticatedWithPasswordAndOttThenPermits() throws Exception {
56+
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
57+
// @formatter:off
58+
this.mockMvc.perform(get("/"))
59+
.andExpect(status().isOk())
60+
.andExpect(authenticated().withUsername("user"));
61+
// @formatter:on
62+
}
63+
64+
@Test
65+
@WithMockUser(authorities = "FACTOR_PASSWORD")
66+
void getWhenAuthenticatedWithPasswordThenRedirectsToOtt() throws Exception {
67+
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
68+
// @formatter:off
69+
this.mockMvc.perform(get("/"))
70+
.andExpect(status().is3xxRedirection())
71+
.andExpect(redirectedUrl("http://localhost/login?factor=ott"));
72+
// @formatter:on
73+
}
74+
75+
@Test
76+
@WithMockUser(authorities = "FACTOR_OTT")
77+
void getWhenAuthenticatedWithOttThenRedirectsToPassword() throws Exception {
78+
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
79+
// @formatter:off
80+
this.mockMvc.perform(get("/"))
81+
.andExpect(status().is3xxRedirection())
82+
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
83+
// @formatter:on
84+
}
85+
86+
@Test
87+
@WithMockUser
88+
void getWhenAuthenticatedThenRedirectsToPassword() throws Exception {
89+
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
90+
// @formatter:off
91+
this.mockMvc.perform(get("/"))
92+
.andExpect(status().is3xxRedirection())
93+
.andExpect(redirectedUrl("http://localhost/login?factor=password"));
94+
// @formatter:on
95+
}
96+
97+
@Test
98+
void getWhenUnauthenticatedThenRedirectsToBoth() throws Exception {
99+
this.spring.register(UseAuthorizationManagerFactoryConfiguration.class, Http200Controller.class).autowire();
100+
// @formatter:off
101+
this.mockMvc.perform(get("/"))
102+
.andExpect(status().is3xxRedirection())
103+
.andExpect(redirectedUrl("http://localhost/login"));
104+
// @formatter:on
105+
}
106+
107+
@RestController
108+
static class Http200Controller {
109+
@GetMapping("/**")
110+
String ok() {
111+
return "ok";
112+
}
113+
}
114+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
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.web.builders.HttpSecurity;
7+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
8+
import org.springframework.security.core.userdetails.User;
9+
import org.springframework.security.core.userdetails.UserDetailsService;
10+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
11+
import org.springframework.security.web.SecurityFilterChain;
12+
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
13+
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
14+
15+
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
16+
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
17+
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
18+
19+
@EnableWebSecurity
20+
@Configuration(proxyBeanMethods = false)
21+
public class ListAuthoritiesEverywhereConfiguration {
22+
23+
// tag::httpSecurity[]
24+
@Bean
25+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
26+
// @formatter:off
27+
http
28+
.authorizeHttpRequests((authorize) -> authorize
29+
.requestMatchers("/admin/**").access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT"), hasRole("ADMIN"))) // <1>
30+
.anyRequest().access(allOf(hasAuthority("FACTOR_PASSWORD"), hasAuthority("FACTOR_OTT")))
31+
)
32+
.formLogin(Customizer.withDefaults())
33+
.oneTimeTokenLogin(Customizer.withDefaults());
34+
// @formatter:on
35+
return http.build();
36+
}
37+
// end::httpSecurity[]
38+
39+
@Bean
40+
UserDetailsService userDetailsService() {
41+
return new InMemoryUserDetailsManager(
42+
User.withDefaultPasswordEncoder()
43+
.username("user")
44+
.password("password")
45+
.authorities("app")
46+
.build()
47+
);
48+
}
49+
50+
@Bean
51+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
52+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
53+
}
54+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.springframework.security.docs.servlet.authentication.authorizationmanagerfactory;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.authorization.AuthorizationManagerFactory;
6+
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
7+
import org.springframework.security.config.Customizer;
8+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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+
class UseAuthorizationManagerFactoryConfiguration {
20+
21+
// tag::httpSecurity[]
22+
@Bean
23+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
24+
// @formatter:off
25+
http
26+
.authorizeHttpRequests((authorize) -> authorize
27+
.requestMatchers("/admin/**").hasRole("ADMIN")
28+
.anyRequest().authenticated()
29+
)
30+
.formLogin(Customizer.withDefaults())
31+
.oneTimeTokenLogin(Customizer.withDefaults());
32+
// @formatter:on
33+
return http.build();
34+
}
35+
// end::httpSecurity[]
36+
37+
// tag::authorizationManagerFactoryBean[]
38+
@Bean
39+
AuthorizationManagerFactory<Object> authz() {
40+
return DefaultAuthorizationManagerFactory.builder()
41+
.requireAdditionalAuthorities("FACTOR_PASSWORD", "FACTOR_OTT").build();
42+
}
43+
// end::authorizationManagerFactoryBean[]
44+
45+
@Bean
46+
UserDetailsService userDetailsService() {
47+
return new InMemoryUserDetailsManager(
48+
User.withDefaultPasswordEncoder()
49+
.username("user")
50+
.password("password")
51+
.authorities("app")
52+
.build()
53+
);
54+
}
55+
56+
@Bean
57+
OneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler() {
58+
return new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
59+
}
60+
}

0 commit comments

Comments
 (0)