1
0
mirror of synced 2026-05-22 21:33:16 +00:00

AuthorizationManagerFactories.when

Closes gh-18920
This commit is contained in:
Robert Winch
2026-03-11 13:56:04 -05:00
parent 8224b16caf
commit 28acf62936
6 changed files with 128 additions and 80 deletions
@@ -17,6 +17,13 @@
package org.springframework.security.authorization;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* Creates common {@link AuthorizationManagerFactory} instances.
@@ -56,6 +63,38 @@ public final class AuthorizationManagerFactories {
private final AllRequiredFactorsAuthorizationManager.Builder<T> factors = AllRequiredFactorsAuthorizationManager
.builder();
private @Nullable Predicate<Authentication> whenCondition;
/**
* Apply the required factors only when the given condition is true for the
* current {@link Authentication}. When the condition is false, no additional
* factors are required (equivalent to permit-all for the additional
* authorization). Implemented using
* {@link ConditionalAuthorizationManager#when(java.util.function.Predicate)}.
* @param condition the condition to evaluate (must not be null)
* @return the {@link AdditionalRequiredFactorsBuilder} to further customize
* @since 7.1
*/
public AdditionalRequiredFactorsBuilder<T> when(Predicate<Authentication> condition) {
Assert.notNull(condition, "condition cannot be null");
this.whenCondition = condition;
return this;
}
/**
* Customize the condition that determines if the required factors are evaluated.
* @param condition a function that takes the current condition and returns the
* new condition
* @return the {@link AdditionalRequiredFactorsBuilder} to further customize
* @since 7.1
*/
public AdditionalRequiredFactorsBuilder<T> withWhen(
Function<@Nullable Predicate<Authentication>, @Nullable Predicate<Authentication>> condition) {
Assert.notNull(condition, "condition cannot be null");
this.whenCondition = condition.apply(this.whenCondition);
return this;
}
/**
* Add additional authorities that will be required.
* @param additionalAuthorities the additional authorities.
@@ -89,7 +128,12 @@ public final class AuthorizationManagerFactories {
*/
public DefaultAuthorizationManagerFactory<T> build() {
DefaultAuthorizationManagerFactory<T> result = new DefaultAuthorizationManagerFactory<>();
AllRequiredFactorsAuthorizationManager<T> additionalChecks = this.factors.build();
AuthorizationManager<T> additionalChecks = this.factors.build();
if (this.whenCondition != null) {
additionalChecks = ConditionalAuthorizationManager.<T>when(this.whenCondition)
.whenTrue(additionalChecks)
.build();
}
result.setAdditionalAuthorization(additionalChecks);
return result;
}
@@ -19,8 +19,10 @@ package org.springframework.security.authorization;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authentication.TestingAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@@ -311,6 +313,59 @@ public class AuthorizationManagerFactoryTests {
.isTrue();
}
@Test
public void builderWhenWhenConditionThenAdditionalFactorsRequiredOnlyWhenConditionTrue() {
AuthorizationManagerFactory<String> factory = AuthorizationManagerFactories.<String>multiFactor()
.requireFactors("ROLE_ADMIN")
.when((auth) -> "admin".equals(auth.getName()))
.build();
// When condition is true (admin user), ROLE_ADMIN is required in addition to
// hasRole("USER")
assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedAdmin(), "").isGranted())
.isTrue();
// When condition is false (non-admin user), additional factors are not required
assertUserGranted(factory.hasRole("USER"));
}
@Test
public void builderWhenWhenConditionFalseThenUserWithoutRequiredFactorGranted() {
AuthorizationManagerFactory<String> factory = AuthorizationManagerFactories.<String>multiFactor()
.requireFactors("ROLE_ADMIN")
.when((auth) -> "admin".equals(auth.getName()))
.build();
// Non-admin user does not need ROLE_ADMIN for hasRole("USER")
assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted())
.isTrue();
}
@Test
public void builderWhenWithWhenConditionThenConditionIsCustomized() {
AuthorizationManagerFactory<String> factory = AuthorizationManagerFactories.<String>multiFactor()
.requireFactors("ROLE_ADMIN")
.when((auth) -> "admin".equals(auth.getName()))
.withWhen((current) -> (auth) -> current != null && current.test(auth) && auth.isAuthenticated())
.build();
// When condition is true (admin user and authenticated), ROLE_ADMIN is required
assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedAdmin(), "").isGranted())
.isTrue();
// When condition is false (admin user but not authenticated), additional factors
// are not required
TestingAuthenticationToken unauthenticatedAdmin = new TestingAuthenticationToken("admin", "password",
"ROLE_USER");
unauthenticatedAdmin.setAuthenticated(false);
assertThat(factory.hasRole("USER").authorize(() -> unauthenticatedAdmin, "").isGranted()).isTrue();
// When condition is false (non-admin user), additional factors are not required
assertUserGranted(factory.hasRole("USER"));
}
@Test
public void builderWhenWithWhenNullThenIllegalArgumentException() {
AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object> builder = AuthorizationManagerFactories
.multiFactor();
assertThatIllegalArgumentException().isThrownBy(() -> builder.withWhen(null))
.withMessage("condition cannot be null");
}
private void assertUserGranted(AuthorizationManager<String> manager) {
assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isTrue();
}
@@ -108,23 +108,20 @@ include-code::./ValidDurationConfiguration[tag=httpSecurity,indent=0]
In our previous examples, MFA is a static decision per request.
There are times when we might want to require MFA for some users, but not others.
Determining if MFA is enabled per user can be achieved by creating a custom `AuthorizationManager` that conditionally requires factors based upon the `Authentication`.
Determining if MFA is enabled per user can be achieved by using `AuthorizationManagerFactories.multiFactor().when` to conditionally require factors based upon the `Authentication`.
This is implemented using xref:servlet/authorization/architecture.adoc#authz-conditional-authorization-manager[`ConditionalAuthorizationManager`].
include-code::./AdminMfaAuthorizationManagerConfiguration[tag=authorizationManager,indent=0]
<1> MFA is required for the user with the username `admin`
<2> Otherwise, MFA is not required
To enable the MFA rules globally, we can publish an `AuthorizationManagerFactory` Bean.
To enable the conditional MFA rules globally, we can publish an `AuthorizationManagerFactory` Bean.
include-code::./AdminMfaAuthorizationManagerConfiguration[tag=authorizationManagerFactory,indent=0]
<1> Inject the custom `AuthorizationManager` as the javadoc:org.springframework.security.authorization.DefaultAuthorizationManagerFactory#setAdditionalAuthorization(org.springframework.security.authorization.AuthorizationManager)[DefaultAuthorization.additionalAuthorization].
This instructs `DefaultAuthorizationManagerFactory` that any authorization rule should apply our custom `AuthorizationManager` along with any authorization requirements defined by the application (e.g. `hasRole("ADMIN")`).
<2> Publish `DefaultAuthorizationManagerFactory` as a Bean, so it is used globally
<1> Require `FACTOR_OTT` and `FACTOR_PASSWORD`
<2> Only apply the requirement if the username is `admin`. Otherwise, MFA is not required.
<3> Return the `AuthorizationManagerFactory` using `.build()`. Since it is published as a Bean, it is used globally.
This should feel very similar to our previous example in xref:./mfa.adoc#authorization-manager-factory[].
The difference is that in the previous example, the `AuthorizationManagerFactories` is setting `DefaultAuthorization.additionalAuthorization` with a built in `AuthorizationManager` that always requires the same authorities.
The difference is that in the previous example, the `AuthorizationManagerFactories` creates an `AuthorizationManager` that always requires the same authorities.
We can now define our authorization rules which are combined with `AdminMfaAuthorizationManager`.
We can now define our authorization rules which are combined with `AuthorizationManagerFactory`.
include-code::./AdminMfaAuthorizationManagerConfiguration[tag=httpSecurity,indent=0]
<1> URLs that begin with `/admin/**` require `ROLE_ADMIN`.
@@ -138,7 +135,7 @@ If we preferred, we could change our logic to enable MFA based upon the roles ra
[[raam-mfa]]
== RequiredAuthoritiesAuthorizationManager
We've demonstrated how we can dynamically determine the authorities for a particular user in xref:./mfa.adoc#programmatic-mfa[] using a custom `AuthorizationManager`.
We've demonstrated how we can dynamically determine the authorities for a particular user in xref:./mfa.adoc#programmatic-mfa[] using `AuthorizationManagerFactories.multiFactor().when`.
However, this is such a common scenario that Spring Security provides built in support using javadoc:org.springframework.security.authorization.RequiredAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.RequiredAuthoritiesRepository[].
Let's implement the same requirement that we did in xref:./mfa.adoc#programmatic-mfa[] using the built-in support.
@@ -168,7 +165,7 @@ Our example uses an in memory mapping of usernames to the additional required au
For more dynamic use cases that can be determined by the username, a custom implementation of javadoc:org.springframework.security.authorization.RequiredAuthoritiesRepository[] can be created.
Possible examples would be looking up if a user has enabled MFA in an explicit setting, determining if a user has registered a passkey, etc.
For cases that need to determine MFA based upon the `Authentication`, a custom `AuthorizationManger` can be used as demonstrated in xref:./mfa.adoc#programmatic-mfa[].
For cases that need to determine MFA based upon the `Authentication`, `AuthorizationManagerFactories.multiFactor().when` can be used as demonstrated in xref:./mfa.adoc#programmatic-mfa[].
[[hasallauthorities]]
+1
View File
@@ -6,6 +6,7 @@
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.web.util.matcher.InetAddressMatcher[]
* https://github.com/spring-projects/spring-security/issues/18755[gh-18755] - Include `charset` in `WWW-Authenticate` header
* Added xref:servlet/authorization/architecture.adoc#authz-conditional-authorization-manager[ConditionalAuthorizationManager]
* Added `when` and `withWhen` conditions to `AuthorizationManagerFactories.multiFactor()` for xref:servlet/authentication/mfa.adoc#programmatic-mfa[Programmatic MFA]
== OAuth 2.0
@@ -1,17 +1,9 @@
package org.springframework.security.docs.servlet.authentication.programmaticmfa;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AllAuthoritiesAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationManagerFactories;
import org.springframework.security.authorization.AuthorizationManagerFactory;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.DefaultAuthorizationManagerFactory;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -23,7 +15,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler;
import org.springframework.stereotype.Component;
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@@ -46,36 +37,16 @@ class AdminMfaAuthorizationManagerConfiguration {
}
// end::httpSecurity[]
// tag::authorizationManager[]
@Component
class AdminMfaAuthorizationManager implements AuthorizationManager<Object> {
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication, Object context) {
if ("admin".equals(authentication.get().getName())) {
AuthorizationManager<Object> admins =
AllAuthoritiesAuthorizationManager.hasAllAuthorities(
FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY
);
// <1>
return admins.authorize(authentication, context);
} else {
// <2>
return new AuthorizationDecision(true);
}
}
}
// end::authorizationManager[]
// tag::authorizationManagerFactory[]
@Bean
AuthorizationManagerFactory<Object> authorizationManagerFactory(
AdminMfaAuthorizationManager admins) {
DefaultAuthorizationManagerFactory<Object> defaults = new DefaultAuthorizationManagerFactory<>();
AuthorizationManagerFactory<Object> authorizationManagerFactory() {
// <3>
return AuthorizationManagerFactories.multiFactor()
// <1>
defaults.setAdditionalAuthorization(admins);
.requireFactors(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY)
// <2>
return defaults;
.when((auth) -> "admin".equals(auth.getName()))
.build();
}
// end::authorizationManagerFactory[]
@@ -14,8 +14,6 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler
import org.springframework.security.web.authentication.ott.RedirectOneTimeTokenGenerationSuccessHandler
import org.springframework.stereotype.Component
import java.util.function.Supplier
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@@ -40,34 +38,16 @@ internal class AdminMfaAuthorizationManagerConfiguration {
}
// end::httpSecurity[]
// tag::authorizationManager[]
@Component
internal open class AdminMfaAuthorizationManager : AuthorizationManager<Any> {
override fun authorize(
authentication: Supplier<out Authentication?>, context: Any): AuthorizationResult {
return if ("admin" == authentication.get().name) {
var admins =
AllAuthoritiesAuthorizationManager.hasAllAuthorities<Any>(
FactorGrantedAuthority.OTT_AUTHORITY,
FactorGrantedAuthority.PASSWORD_AUTHORITY)
// <1>
admins.authorize(authentication, context)
} else {
// <2>
AuthorizationDecision(true)
}
}
}
// end::authorizationManager[]
// tag::authorizationManagerFactory[]
@Bean
fun authorizationManagerFactory(admins: AdminMfaAuthorizationManager): AuthorizationManagerFactory<Any> {
val defaults = DefaultAuthorizationManagerFactory<Any>()
fun authorizationManagerFactory(): AuthorizationManagerFactory<Any> {
// <3>
return AuthorizationManagerFactories.multiFactor<Any>()
// <1>
defaults.setAdditionalAuthorization(admins)
.requireFactors(FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY)
// <2>
return defaults
.`when` { auth -> "admin" == auth.name }
.build()
}
// end::authorizationManagerFactory[]