Skip to content

SwitchUserGrantedAuthority cannot be deserialized in Webflux #17041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
JohnNiang opened this issue May 6, 2025 · 4 comments · May be fixed by #17064
Closed

SwitchUserGrantedAuthority cannot be deserialized in Webflux #17041

JohnNiang opened this issue May 6, 2025 · 4 comments · May be fixed by #17064
Labels
in: web An issue in web modules (web, webmvc) status: duplicate A duplicate of another issue type: enhancement A general enhancement

Comments

@JohnNiang
Copy link
Contributor

JohnNiang commented May 6, 2025

Describe the bug

I used SwitchUserWebfilter to implement an impersonation function, but it didn't work very well, pleas see the log below:

I found a similar issue in #11775, but the PR #11758 only resolved in Servlet.

Halo  | Caused by: com.fasterxml.jackson.databind.JsonMappingException: The class with org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority and name of org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"])
Halo  | 	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:401) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:360) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1964) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:401) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:220) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:170) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:136) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1382) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4931) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3970) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at org.springframework.data.redis.serializer.JacksonObjectReader.lambda$create$0(JacksonObjectReader.java:54) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:309) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:281) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:46) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.read(RedisSerializationContext.java:277) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.core.DefaultReactiveHashOperations.readHashValue(DefaultReactiveHashOperations.java:292) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at org.springframework.data.redis.core.DefaultReactiveHashOperations.deserializeHashEntry(DefaultReactiveHashOperations.java:307) ~[spring-data-redis-3.4.5.jar:3.4.5]
Halo  | 	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) ~[reactor-core-3.7.5.jar:3.7.5]
Halo  | 	at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmit(FluxFlatMap.java:547) ~[reactor-core-3.7.5.jar:3.7.5]
Halo  | 	at reactor.core.publisher.FluxFlatMap$FlatMapInner.onNext(FluxFlatMap.java:988) ~[reactor-core-3.7.5.jar:3.7.5]
Halo  | 	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122) ~[reactor-core-3.7.5.jar:3.7.5]
Halo  | 	at io.lettuce.core.RedisPublisher$ImmediateSubscriber.onNext(RedisPublisher.java:895) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.RedisPublisher$RedisSubscription.onNext(RedisPublisher.java:295) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.output.StreamingOutput$Subscriber.onNext(StreamingOutput.java:49) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.output.KeyValueListOutput.set(KeyValueListOutput.java:61) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.RedisStateMachine.safeSet(RedisStateMachine.java:814) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.RedisStateMachine.handleBytes(RedisStateMachine.java:601) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.RedisStateMachine$State$Type.handle(RedisStateMachine.java:210) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.RedisStateMachine.doDecode(RedisStateMachine.java:363) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.RedisStateMachine.decode(RedisStateMachine.java:324) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:844) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.CommandHandler.decode0(CommandHandler.java:795) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:769) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:661) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:601) ~[lettuce-core-6.4.2.RELEASE.jar:6.4.2.RELEASE/f4dfb40]
Halo  | 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1357) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:868) ~[netty-transport-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:799) ~[netty-transport-classes-epoll-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:501) ~[netty-transport-classes-epoll-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:399) ~[netty-transport-classes-epoll-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998) ~[netty-common-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.119.Final.jar:4.1.119.Final]
Halo  | 	at java.base/java.lang.Thread.run(Unknown Source) ~[na:na]
Halo  | Caused by: java.lang.IllegalArgumentException: The class with org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority and name of org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details
Halo  | 	at org.springframework.security.jackson2.SecurityJackson2Modules$AllowlistTypeIdResolver.typeFromId(SecurityJackson2Modules.java:293) ~[spring-security-core-6.4.5.jar:6.4.5]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:159) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:151) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:136) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromAny(AsPropertyTypeDeserializer.java:240) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializerNR.deserializeWithType(UntypedObjectDeserializerNR.java:112) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4904) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3036) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at org.springframework.security.jackson2.AbstractUnmodifiableCollectionDeserializer.deserialize(AbstractUnmodifiableCollectionDeserializer.java:51) ~[spring-security-core-6.4.5.jar:6.4.5]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:123) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:56) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:285) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4904) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3061) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at org.springframework.security.jackson2.UsernamePasswordAuthenticationTokenDeserializer.deserialize(UsernamePasswordAuthenticationTokenDeserializer.java:78) ~[spring-security-core-6.4.5.jar:6.4.5]
Halo  | 	at org.springframework.security.jackson2.UsernamePasswordAuthenticationTokenDeserializer.deserialize(UsernamePasswordAuthenticationTokenDeserializer.java:51) ~[spring-security-core-6.4.5.jar:6.4.5]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:170) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:136) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:399) ~[jackson-databind-2.18.3.jar:2.18.3]
Halo  | 	... 48 common frames omitted

To Reproduce

  1. Add additional web filter: SwitchUserWebFilter
        var filter = new SwitchUserWebFilter(userDetailsService, "/", "/login?error=impersonate");
        http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
  1. Add spring-session-data-redis dependency
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
    implementation 'org.springframework.session:spring-session-data-redis'
  1. Start a Redis instance
  2. Try to switch user
  3. See the error

Expected behavior

Should work with session deserialization.

I'm willing to propose a PR to resolve the issue.

Sample

import static org.junit.jupiter.api.Assertions.assertInstanceOf;

import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
import org.springframework.security.web.server.jackson2.WebServerJackson2Module;

class SwitchUserAuthorityTest {

    @Test
    void deserializeTest() throws JsonProcessingException {
        var objectMapper = Jackson2ObjectMapperBuilder.json()
            .modules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader()))
            .indentOutput(true)
            .build();

        var authentication = UsernamePasswordAuthenticationToken.authenticated(
            "admin", "openadmin", AuthorityUtils.createAuthorityList("ROLE_ADMIN")
        );
        var switchUserGrantedAuthority =
            new SwitchUserGrantedAuthority("ADMIN", authentication);
        var extendedAuthorities = new ArrayList<>(authentication.getAuthorities());
        extendedAuthorities.add(switchUserGrantedAuthority);
        authentication = UsernamePasswordAuthenticationToken.authenticated(
            authentication.getPrincipal(), authentication.getCredentials(), extendedAuthorities
        );

        var json = objectMapper.writeValueAsString(authentication);
        var resolved = objectMapper.readValue(json, Authentication.class);
        assertInstanceOf(UsernamePasswordAuthenticationToken.class, resolved);
    }
}
@JohnNiang JohnNiang added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels May 6, 2025
@jzheaux
Copy link
Contributor

jzheaux commented May 6, 2025

Thanks for the report, @JohnNiang. Are you able to contribute a PR that adds this to WebServerJackson2Module?

@jzheaux jzheaux added in: web An issue in web modules (web, webmvc) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels May 6, 2025
@JohnNiang
Copy link
Contributor Author

Hi @jzheaux , I have two approaches to resolve this problem:

  1. Move the MixInAnnotations to WebJackson2Module. This may remove the set in WebServletJackson2Module, I don't know whether it cause a compatibility issue.
  2. Add the MixInAnnotations to WebServerJackson2Module. This may make SwitchUserGrantedAuthorityMixIn public or copy the MixIn into the folder beside WebServerJackson2Module class.

Please help me decide which approach is better.

@jzheaux
Copy link
Contributor

jzheaux commented May 7, 2025

Closing in favor of #17064

@jzheaux jzheaux closed this as completed May 7, 2025
@jzheaux
Copy link
Contributor

jzheaux commented May 7, 2025

Good question, @JohnNiang, I answered in the PR.

@jzheaux jzheaux added the status: duplicate A duplicate of another issue label May 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web An issue in web modules (web, webmvc) status: duplicate A duplicate of another issue type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants