Closed
Description
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
- Add additional web filter: SwitchUserWebFilter
var filter = new SwitchUserWebFilter(userDetailsService, "/", "/login?error=impersonate");
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
- Add spring-session-data-redis dependency
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
implementation 'org.springframework.session:spring-session-data-redis'
- Start a Redis instance
- Try to switch user
- 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);
}
}