Skip to content

Allow non-AuthorizationDecision results in WebSecurity#19284

Open
seonwooj0810 wants to merge 1 commit into
spring-projects:mainfrom
seonwooj0810:gh-19282
Open

Allow non-AuthorizationDecision results in WebSecurity#19284
seonwooj0810 wants to merge 1 commit into
spring-projects:mainfrom
seonwooj0810:gh-19282

Conversation

@seonwooj0810

Copy link
Copy Markdown

Closes gh-19282

Root cause

WebSecurity#addAuthorizationManager registers, for every AuthorizationFilter in a SecurityFilterChain, a lambda that delegates to the filter's AuthorizationManager#authorize so that WebInvocationPrivilegeEvaluator can use the same authorization rules. The lambda casts the returned AuthorizationResult to AuthorizationDecision:

builder.add(securityFilterChain::matches,
        (authentication, context) -> (AuthorizationDecision) authorizationManager
            .authorize(authentication, context.getRequest()));

The cast was added in 0ab01eac1 while migrating from the deprecated AuthorizationManager#check (which returned AuthorizationDecision) to #authorize (which returns the broader AuthorizationResult). The narrowing was carried over unconditionally, which violates the new return-type contract for any AuthorizationResult that is not an AuthorizationDecision.

In Spring Security 7.0 this is reachable through the multi-factor authorization manager produced by AuthorizationManagerFactories. Its underlying AllRequiredFactorsAuthorizationManager#authorize returns a FactorAuthorizationDecision, which implements AuthorizationResult directly and is a sibling of AuthorizationDecision, not a subtype. The stack trace in the issue ends in this lambda:

java.lang.ClassCastException: class FactorAuthorizationDecision cannot be cast to class AuthorizationDecision
    at WebSecurity.lambda$addAuthorizationManager$2(WebSecurity.java:384)
    at RequestMatcherDelegatingAuthorizationManager.authorize(...)
    at AuthorizationManagerWebInvocationPrivilegeEvaluator.isAllowed(...)

The cast is also unnecessary: RequestMatcherDelegatingAuthorizationManager.Builder#add accepts an AuthorizationManager<? super RequestAuthorizationContext>, whose authorize already returns AuthorizationResult.

Change

Remove the cast. The lambda now returns the AuthorizationResult directly, matching the contract of AuthorizationManager#authorize and the type expected by Builder#add.

Tests

Added WebSecurityTests.privilegeEvaluatorWhenAuthorizationManagerReturnsFactorDecisionThenIsAllowedReturnsFalseWithoutClassCastException. It registers a SecurityFilterChain whose authorization manager returns a FactorAuthorizationDecision and asserts that WebInvocationPrivilegeEvaluator#isAllowed reports false without throwing. With the cast still present, this test fails with ClassCastException at WebSecurityTests.java:131; with the cast removed it passes.

./gradlew :spring-security-config:test --tests 'org.springframework.security.config.annotation.web.builders.WebSecurityTests' is green, and ./gradlew :spring-security-config:checkstyleMain :spring-security-config:checkstyleTest reports no violations.

Verification done: (1) gh pr list --search "FactorAuthorizationDecision in:title,body" returns no in-flight PRs; (2) the linked issue has zero comments and no self-claims; (3) the fix is in .java files only; (4) the buggy cast is present on origin/main at WebSecurity.java:383; (5) the issue is a stand-alone bug report, not a sub-issue of an umbrella; (6) the stack trace in the issue terminates inside the framework lambda this PR rewrites.

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 spring-projectsgh-19282

Signed-off-by: seonwoo_jung <laborlawseon@kap.kr>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FactorAuthorizationDecision cannot be cast to class AuthorizationDecision

2 participants