From 65d225b94ac7ce04797cbf636134bb884550d5de Mon Sep 17 00:00:00 2001 From: seonwoo_jung Date: Tue, 9 Jun 2026 06:18:31 +0900 Subject: [PATCH] Allow non-AuthorizationDecision results in WebSecurity WebSecurity#addAuthorizationManager registered a lambda that cast the AuthorizationManager#authorize result to AuthorizationDecision when forwarding the call to the privilege evaluator. The cast was added when migrating away from the deprecated AuthorizationManager#check method but narrowed AuthorizationResult to AuthorizationDecision. With multi-factor authentication, the manager produced by AuthorizationManagerFactories returns a FactorAuthorizationDecision, which implements AuthorizationResult directly and is not assignable to AuthorizationDecision, so WebInvocationPrivilegeEvaluator#isAllowed failed with ClassCastException for any URL guarded by a multi-factor manager. The lambda is registered through Builder#add as an AuthorizationManager whose authorize method already returns AuthorizationResult, so the cast is unnecessary as well as unsound. Closes gh-19282 Signed-off-by: seonwoo_jung --- .../annotation/web/builders/WebSecurity.java | 5 +-- .../web/builders/WebSecurityTests.java | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 63af3affff7..d8dfeda71af 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -379,9 +379,8 @@ private boolean addAuthorizationManager(SecurityFilterChain securityFilterChain, } if (filter instanceof AuthorizationFilter authorization) { AuthorizationManager authorizationManager = authorization.getAuthorizationManager(); - builder.add(securityFilterChain::matches, - (authentication, context) -> (AuthorizationDecision) authorizationManager - .authorize(authentication, context.getRequest())); + builder.add(securityFilterChain::matches, (authentication, context) -> authorizationManager + .authorize(authentication, context.getRequest())); mappings = true; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 34520754ef5..08812308554 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -17,6 +17,7 @@ package org.springframework.security.config.annotation.web.builders; import java.io.IOException; +import java.util.List; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationTextPublisher; @@ -35,13 +36,19 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.FactorAuthorizationDecision; +import org.springframework.security.authorization.RequiredFactor; +import org.springframework.security.authorization.RequiredFactorError; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; +import org.springframework.security.core.authority.FactorGrantedAuthority; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.web.bind.annotation.RequestMapping; @@ -111,6 +118,19 @@ public void requestRejectedHandlerInvokedWhenOperationalObservationRegistry() th assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_BAD_REQUEST); } + // gh-19282 + @Test + public void privilegeEvaluatorWhenAuthorizationManagerReturnsFactorDecisionThenIsAllowedReturnsFalseWithoutClassCastException() { + loadConfig(FactorDecisionConfig.class); + WebInvocationPrivilegeEvaluator evaluator = this.context.getBean(WebInvocationPrivilegeEvaluator.class); + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + // Before gh-19282 the lambda registered in WebSecurity#addAuthorizationManager + // cast the result of AuthorizationManager#authorize to AuthorizationDecision, + // which fails for AuthorizationResult subtypes such as + // FactorAuthorizationDecision. + assertThat(evaluator.isAllowed("/secure", authentication)).isFalse(); + } + // gh-19128 @Test public void ignoringWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception { @@ -263,6 +283,25 @@ String path() { } + // gh-19282 + @Configuration + @EnableWebSecurity + static class FactorDecisionConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((requests) -> requests + .anyRequest().access((authentication, context) -> new FactorAuthorizationDecision( + List.of(RequiredFactorError.createMissing( + RequiredFactor.withAuthority(FactorGrantedAuthority.OTT_AUTHORITY).build()))))); + // @formatter:on + return http.build(); + } + + } + @Configuration @EnableWebSecurity static class RequestRejectedHandlerConfig {