spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.55k stars 5.79k forks source link

NullPointerException when GrantedAuthority.getAuthority() returns null #15243

Open Crystark opened 1 week ago

Crystark commented 1 week ago

Describe the bug When my custom GrantedAuthority returns null on getAuthority(), I get the following exception that makes the application fail.

2024-06-13T17:44:59.000+02:00 ERROR 12657 --- [     parallel-2] a.w.r.e.AbstractErrorWebExceptionHandler : [cdb41c64-1]  500 Server Error for HTTP POST "/graphql"

java.lang.NullPointerException: The mapper [org.springframework.security.authorization.AuthorityReactiveAuthorizationManager$$Lambda/0x0000000800d06000] returned a null value.
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:115) ~[reactor-core-3.6.6.jar:3.6.6]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] :
    reactor.core.publisher.Flux.map(Flux.java:6580)
    org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.check(AuthorityReactiveAuthorizationManager.java:50)
Error has been observed at the following site(s):
    *______________Flux.map ⇢ at org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.check(AuthorityReactiveAuthorizationManager.java:50)
    |_             Flux.any ⇢ at org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.check(AuthorityReactiveAuthorizationManager.java:51)
    |_             Mono.map ⇢ at org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.check(AuthorityReactiveAuthorizationManager.java:52)
    |_  Mono.defaultIfEmpty ⇢ at org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.check(AuthorityReactiveAuthorizationManager.java:53)
    *__________Mono.flatMap ⇢ at org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager.lambda$check$2(DelegatingReactiveAuthorizationManager.java:58)
    *________Flux.concatMap ⇢ at org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager.check(DelegatingReactiveAuthorizationManager.java:54)
    |_            Flux.next ⇢ at org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager.check(DelegatingReactiveAuthorizationManager.java:64)
    |_  Mono.defaultIfEmpty ⇢ at org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager.check(DelegatingReactiveAuthorizationManager.java:65)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.authorization.ObservationReactiveAuthorizationManager.lambda$check$4(ObservationReactiveAuthorizationManager.java:70)
    |_      Mono.doOnCancel ⇢ at org.springframework.security.authorization.ObservationReactiveAuthorizationManager.lambda$check$4(ObservationReactiveAuthorizationManager.java:76)
    |_       Mono.doOnError ⇢ at org.springframework.security.authorization.ObservationReactiveAuthorizationManager.lambda$check$4(ObservationReactiveAuthorizationManager.java:76)
    *__Mono.deferContextual ⇢ at org.springframework.security.authorization.ObservationReactiveAuthorizationManager.check(ObservationReactiveAuthorizationManager.java:66)
    |_          Mono.filter ⇢ at org.springframework.security.authorization.ReactiveAuthorizationManager.verify(ReactiveAuthorizationManager.java:52)
    |_   Mono.switchIfEmpty ⇢ at org.springframework.security.authorization.ReactiveAuthorizationManager.verify(ReactiveAuthorizationManager.java:53)
    |_         Mono.flatMap ⇢ at org.springframework.security.authorization.ReactiveAuthorizationManager.verify(ReactiveAuthorizationManager.java:54)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.authorization.AuthorizationWebFilter.filter(AuthorizationWebFilter.java:53)
    |_       Mono.doOnError ⇢ at org.springframework.security.web.server.authorization.AuthorizationWebFilter.filter(AuthorizationWebFilter.java:54)
    |_   Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.authorization.AuthorizationWebFilter.filter(AuthorizationWebFilter.java:56)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ AuthorizationWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    |_   Mono.onErrorResume ⇢ at org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter.filter(ExceptionTranslationWebFilter.java:53)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ ExceptionTranslationWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    *__________Mono.flatMap ⇢ at org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter.filter(ServerRequestCacheWebFilter.java:41)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ ServerRequestCacheWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    *_____________Mono.then ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.onAuthenticationSuccess(AuthenticationWebFilter.java:136)
    |_    Mono.contextWrite ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.onAuthenticationSuccess(AuthenticationWebFilter.java:137)
    *__________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:125)
    |_       Mono.doOnError ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:127)
    *__________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:115)
    |_   Mono.onErrorResume ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:116)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    |_    Mono.contextWrite ⇢ at org.springframework.security.web.server.context.ReactorContextWebFilter.filter(ReactorContextWebFilter.java:48)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_           checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    |_    Mono.contextWrite ⇢ at org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter.filter(ServerHttpSecurity.java:3968)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.wrapFilter(ObservationWebFilterChainDecorator.java:211)
    |_     Mono.doOnSuccess ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$AroundWebFilterObservation$SimpleAroundWebFilterObservation.lambda$wrap$6(ObservationWebFilterChainDecorator.java:367)
    |_      Mono.doOnCancel ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$AroundWebFilterObservation$SimpleAroundWebFilterObservation.lambda$wrap$6(ObservationWebFilterChainDecorator.java:368)
    |_       Mono.doOnError ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$AroundWebFilterObservation$SimpleAroundWebFilterObservation.lambda$wrap$6(ObservationWebFilterChainDecorator.java:369)
    |_    Mono.contextWrite ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$AroundWebFilterObservation$SimpleAroundWebFilterObservation.lambda$wrap$6(ObservationWebFilterChainDecorator.java:373)
    *__Mono.deferContextual ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilter.filter(ObservationWebFilterChainDecorator.java:193)
    |_           checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.security.web.server.ObservationWebFilterChainDecorator$ObservationWebFilterChain.filter(ObservationWebFilterChainDecorator.java:152)
    *__________Mono.flatMap ⇢ at org.springframework.security.web.server.WebFilterChainProxy.filter(WebFilterChainProxy.java:63)
    |_           checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
    |_       Mono.doFinally ⇢ at io.sentry.spring.jakarta.webflux.SentryWebFilter.filter(SentryWebFilter.java:39)
    |_       Mono.doOnError ⇢ at io.sentry.spring.jakarta.webflux.SentryWebFilter.filter(SentryWebFilter.java:40)
    |_         Mono.doFirst ⇢ at io.sentry.spring.jakarta.webflux.SentryWebFilter.filter(SentryWebFilter.java:41)
    |_           checkpoint ⇢ io.sentry.spring.jakarta.webflux.SentryWebFilter [DefaultWebFilterChain]
    *____________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
    |_       Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
    |_   Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
    |_       Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
    |_   Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
    |_       Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
    *____________Mono.error ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler$CheckpointInsertingHandler.handle(ExceptionHandlingWebHandler.java:106)
    |_           checkpoint ⇢ HTTP POST "/graphql" [ExceptionHandlingWebHandler]
    *__________Mono.flatMap ⇢ at io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler.handle(SentryWebExceptionHandler.java:70)
Original Stack Trace:
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:115) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:453) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onNext(FluxFlattenIterable.java:256) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:158) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.request(FluxMapFuseable.java:360) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.request(MonoFlatMap.java:194) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.request(FluxFilterFuseable.java:411) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.request(FluxMapFuseable.java:360) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.request(FluxMapFuseable.java:360) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onSubscribe(FluxFlattenIterable.java:241) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onSubscribe(FluxMapFuseable.java:265) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onSubscribe(FluxMapFuseable.java:265) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onSubscribe(FluxFilterFuseable.java:305) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:117) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onSubscribe(FluxMapFuseable.java:265) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:55) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onNext(FluxPeekFuseable.java:503) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:492) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:424) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:210) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:373) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4568) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onNext(FluxConcatMapNoPrefetch.java:207) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerComplete(FluxConcatMapNoPrefetch.java:275) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onComplete(FluxConcatMap.java:889) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:189) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onComplete(FluxFilterFuseable.java:171) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onComplete(FluxPeekFuseable.java:595) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:85) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2573) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2241) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4568) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:82) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.onComplete(MonoNext.java:102) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onComplete(FluxFilterFuseable.java:171) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:850) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:612) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:592) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFlatMap$FlatMapMain.request(FluxFlatMap.java:349) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.request(Operators.java:2331) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.request(FluxPeekFuseable.java:437) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.request(MonoFlatMap.java:194) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.request(Operators.java:2331) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.request(FluxConcatMapNoPrefetch.java:339) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.request(FluxDefaultIfEmpty.java:98) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.request(FluxPeekFuseable.java:437) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2241) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:152) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onSubscribe(FluxPeekFuseable.java:471) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:152) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$BaseFluxToMonoOperator.onSubscribe(Operators.java:2051) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.onSubscribe(MonoNext.java:70) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onSubscribe(FluxConcatMapNoPrefetch.java:164) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:55) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:134) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onComplete(FluxFilterFuseable.java:171) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onComplete(FluxMapFuseable.java:350) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1866) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.signalCached(MonoCacheTime.java:337) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.onNext(MonoCacheTime.java:354) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:210) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:181) ~[reactor-core-3.6.6.jar:3.6.6]
        at io.sentry.spring.jakarta.webflux.SentryScheduleHook.lambda$apply$0(SentryScheduleHook.java:23) ~[sentry-spring-jakarta-7.9.0.jar:na]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.6.6.jar:3.6.6]
        at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.6.6.jar:3.6.6]
        at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:317) ~[na:na]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

To Reproduce I can't provide a complete working example but hopefully the following samples will help:

First here is a condensed version of my base security config so that I can work with my permissions provider. The key points are my asking of the access authority and that I can return null as a response to getAuthority which is allowed as per the documentation:

Returns: a representation of the granted authority (or null if the granted authority cannot be expressed as a String with sufficient precision).

@Configuration
class BaseSecurityConfig {
    @Bean
    fun reactiveJwtAuthenticationConverter() = ReactiveJwtAuthenticationConverter().apply {
        setJwtGrantedAuthoritiesConverter { jwt ->
            jwt.getClaim<Map<String, List<Map<String, Any>>>>("authorization")
                ?.get("permissions")
                ?.let { Flux.fromIterable(it) }
                ?.flatMap(::permissionToMyAuthority)
                ?: Flux.empty()
        }
    }

    @Bean
    fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http {
        csrf { disable() }
        formLogin { disable() }
        logout { disable() }
        httpBasic { disable() }
        authorizeExchange {
            authorize(anyExchange, hasAuthority("access"))
        }
        oauth2ResourceServer { jwt {} }
    }
}

fun permissionToMyAuthority(permission: Map<String, Any?>) = (permission["scopes"] as List<*>?)
    .takeUnless { it.isNullOrEmpty() }
    ?.let { scopes ->
        val rsname = permission["rsname"] as String?

        Flux.fromIterable(scopes)
            .cast(String::class.java) // We know we only have strings
            .map { MyAuthority(it, rsname) }
    } ?: Flux.empty()

data class MyAuthority(private val scope: String, private val resource: String?) : GrantedAuthority {

    override fun getAuthority(): String? = scope.takeIf { resource == null }

    fun hasPermission(scope: String, type: String, reference: String) =
        this.scope == scope && nameResource(type, reference) == resource
}

fun nameResource(type: String, reference: String) = "$type:$reference"

It seems that the order in which I process the permissions matter as if it finds the access authority it won't continue and I won't get the NPE. However if I have my nullable authority first it's going to fail.

Expected behavior I expect org.springframework.security.authorization.AuthorityReactiveAuthorizationManager#check to not crash on a null authority and just filter it out.

Code proposition

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
        // @formatter:off
        return authentication.filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .mapNotNull(GrantedAuthority::getAuthority)
                .any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority)))
                .map((granted) -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(granted, this.authorities)))
                .defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities));
        // @formatter:on
    }
sjohnr commented 1 week ago

Thanks for reaching out @Crystark!

I can return null as a response to getAuthority which is allowed as per the documentation:

Please consider the entirety of the method's documentation:

If the GrantedAuthority can be represented as a String and that String is sufficient in precision to be relied upon for an access control decision by an AccessDecisionManager (or delegate), this method should return such a String.

If the GrantedAuthority cannot be expressed with sufficient precision as a String, null should be returned. Returning null will require an AccessDecisionManager (or delegate) to specifically support the GrantedAuthority implementation, so returning null should be avoided unless actually required.

Note: Unfortunately, this documentation doesn't mention AuthorizationManager (or the reactive counterpart) so I think it would be worth updating this documentation.

The point the docs are making is that the thing consuming the GrantedAuthority has to understand null and/or your GrantedAuthority implementation. The AuthorityReactiveAuthorizationManager does not handle this, and so you would need to provide a custom implementation.

I expect org.springframework.security.authorization.AuthorityReactiveAuthorizationManager#check to not crash on a null authority and just filter it out.

I'm not really sure it is valuable or desirable to handle null authorities by default, as this would likely hide a problem from the user. In your case, what is expected to handle the authority that is returning null?

This is actually an indication that you may have reversed the role of ReactiveAuthorizationManager and GrantedAuthority by placing logic in the GrantedAuthority itself, and in fact the logic in your custom authority should be extracted into a custom ReactiveAuthorizationManager instead.

Having said all of that, I feel that instead of ignoring null, it could be beneficial for AuthorityReactiveAuthorizationManager to return a more informative error message using Assert.hasText(). Would you be interested in submitting a PR for this?

Crystark commented 1 week ago

Thanks for getting back to me so fast on this @sjohnr

In your case, what is expected to handle the authority that is returning null?

The authority returning null is used in my PermissionEvaluator triggered on @PreAuthorize:

class MyPermissionEvaluator : PermissionEvaluator {
    override fun hasPermission(authentication: Authentication, targetDomainObject: Any?, permission: Any): Boolean {
        throw NotImplementedError("Use hasPermission(targetId, targetType, permission) or hasAuthority(authority) instead")
    }

    override fun hasPermission(
        authentication: Authentication,
        targetId: Serializable?,
        targetType: String,
        permission: Any
    ): Boolean =
        if (targetId == null) {
            log.warn(
                "Permission denied: unexpected null 'targetId' argument for target type '{}' and permission '{}'",
                targetType,
                permission
            )
            false
        } else if (permission !is String) {
            log.warn("Permission denied: 'permission' argument ({}) should be a String", permission.javaClass.name)
            false
        } else {
            authentication.authorities
                .filterIsInstance<MyAuthority>()
                .any { it.hasPermission(permission, targetType, targetId.toString()) }
        }

    companion object {
        val log: Logger = LoggerFactory.getLogger(MyPermissionEvaluator::class.java)
    }
}

And this is how I use this:

@PreAuthorize("hasPermission(#someId, '$RESOURCE_TYPE', '$RESOURCE_SCOPE')")

This is working fine by the way. I only got the error I mentioned when the null-returning GrantedAuthority was moved in the JWT token before the access-returning one and because I'm using hasAuthority("access")

So I'm not directly using AuthorityReactiveAuthorizationManager right ? Maybe I should ?

The reason I did everything like that is that I wanted to be able to extract the various permissions from my JWT token and make them available to my PermissionEvaluator as well as be able to use hasAuthority("access"). Maybe I'm mixing up things that should not be ? Do you have any suggestions ? I'm already thinking I can just return a random string instead of null 😏 but that feels a bit hacky and I'd rather try and do this the right way. Well actually I would probably concatenate the scope and the permission separated by # but still doesn't really feel right.

Thanks

sjohnr commented 1 week ago

@Crystark thanks for your reply.

While reviewing your code, I am having a bit of difficulty understanding it and the intent of it, but it appears to be all about a resource name taken from a JWT and combined with a list of scopes. There’s a lot to unpack there and this isn’t really the place to work through your solution. I can tell you however that I’m fairly certain there are improvements to be made. If you would like to work through that, please open a stack overflow question and provide the link here and I’ll be happy to take a look.

In any case, I think returning null for the authority requires a custom AuthorizationManager which you have not provided. I feel the best we can do here is throw a more informative exception (perhaps with Assert.hasText(...)).