From 1d28582145c1fc39b18e4d9aec5800b22a9dbc79 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Thu, 19 Mar 2026 19:42:59 +0300 Subject: [PATCH] Add unless support to AdditionalRequiredFactorsBuilder In this commit, we are adding support for 'unless' to 'AdditionalRequiredFactorsBuilder'. This is useful if we want a specific user type or factor to disable MFA. Closes: gh-18925 Signed-off-by: Andrey Litvitski --- .../AuthorizationManagerFactories.java | 39 +++++++++++++++++++ .../AuthorizationManagerFactoryTests.java | 37 ++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactories.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactories.java index b48bf667097..02c224a3588 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactories.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagerFactories.java @@ -29,6 +29,7 @@ * Creates common {@link AuthorizationManagerFactory} instances. * * @author Rob Winch + * @author Andrey Litvitski * @since 7.0 * @see DefaultAuthorizationManagerFactory */ @@ -57,6 +58,7 @@ public static AdditionalRequiredFactorsBuilder multiFactor() { * * @param the type for the {@link DefaultAuthorizationManagerFactory} * @author Rob Winch + * @author Andrey Litvitski */ public static final class AdditionalRequiredFactorsBuilder { @@ -65,6 +67,8 @@ public static final class AdditionalRequiredFactorsBuilder { private @Nullable Predicate whenCondition; + private @Nullable Predicate unlessCondition; + /** * Apply the required factors only when the given condition is true for the * current {@link Authentication}. When the condition is false, no additional @@ -81,6 +85,21 @@ public AdditionalRequiredFactorsBuilder when(Predicate condit return this; } + /** + * Skip the required factors when the given condition is true for the current + * {@link Authentication}. When the condition is true, no additional factors are + * required. 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 unless(Predicate condition) { + Assert.notNull(condition, "condition cannot be null"); + this.unlessCondition = 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 @@ -95,6 +114,20 @@ public AdditionalRequiredFactorsBuilder withWhen( return this; } + /** + * Customize the condition that determines if the required factors are skipped. + * @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 withUnless( + Function<@Nullable Predicate, @Nullable Predicate> condition) { + Assert.notNull(condition, "condition cannot be null"); + this.unlessCondition = condition.apply(this.unlessCondition); + return this; + } + /** * Add additional authorities that will be required. * @param additionalAuthorities the additional authorities. @@ -134,6 +167,12 @@ public DefaultAuthorizationManagerFactory build() { .whenTrue(additionalChecks) .build(); } + if (this.unlessCondition != null) { + additionalChecks = ConditionalAuthorizationManager.when(this.unlessCondition) + .whenTrue(SingleResultAuthorizationManager.permitAll()) + .whenFalse(additionalChecks) + .build(); + } result.setAdditionalAuthorization(additionalChecks); return result; } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java index 230c166bae7..375b6c704fd 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java @@ -35,6 +35,7 @@ * Tests for {@link AuthorizationManagerFactory}. * * @author Steve Riesenberg + * @author Andrey Litvitski */ public class AuthorizationManagerFactoryTests { @@ -366,6 +367,42 @@ public void builderWhenWithWhenNullThenIllegalArgumentException() { .withMessage("condition cannot be null"); } + @Test + public void builderWhenUnlessConditionFalseThenRequiredFactorsEnforced() { + AuthorizationManagerFactory factory = AuthorizationManagerFactories.multiFactor() + .requireFactors("ROLE_ADMIN") + .unless((auth) -> "bearer".equals(auth.getName())) + .build(); + assertUserDenied(factory.hasRole("USER")); + } + + @Test + public void builderWhenUnlessConditionTrueThenMfaSkipped() { + AuthorizationManagerFactory factory = AuthorizationManagerFactories.multiFactor() + .requireFactors("ROLE_ADMIN") + .unless((auth) -> "bearer".equals(auth.getName())) + .build(); + assertThat(factory.hasRole("USER") + .authorize(() -> new TestingAuthenticationToken("bearer", "password", "ROLE_USER"), "") + .isGranted()).isTrue(); + } + + @Test + public void builderWhenWithUnlessConditionThenConditionIsCustomized() { + AuthorizationManagerFactory factory = AuthorizationManagerFactories.multiFactor() + .requireFactors("ROLE_ADMIN") + .unless((auth) -> "bearer".equals(auth.getName())) + .withUnless((current) -> (auth) -> current != null && current.test(auth) && auth.isAuthenticated()) + .build(); + assertThat(factory.hasRole("USER") + .authorize(() -> new TestingAuthenticationToken("bearer", "password", "ROLE_USER"), "") + .isGranted()).isTrue(); + TestingAuthenticationToken unauthenticatedBearer = new TestingAuthenticationToken("bearer", "password", + "ROLE_USER"); + unauthenticatedBearer.setAuthenticated(false); + assertThat(factory.hasRole("USER").authorize(() -> unauthenticatedBearer, "").isGranted()).isFalse(); + } + private void assertUserGranted(AuthorizationManager manager) { assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isTrue(); }