spring-attic / spring-security-oauth

Support for adding OAuth1(a) and OAuth2 features (consumer and provider) for Spring web applications.
http://github.com/spring-projects/spring-security-oauth
Apache License 2.0
4.69k stars 4.04k forks source link

OAuth2 authentication + Redis global session causes deserialization exception #1175

Open RenaudDenis opened 6 years ago

RenaudDenis commented 6 years ago

Hi,

I'm trying to combine OAuth2 authentication together with a global Redis session. I'm facing an issue and I'd like to know what could be done to avoid it.

Problem seems to come from the OAuth2ClientContext:

    @Configuration
    protected static class OAuth2ClientContextConfiguration {

        @Resource
        @Qualifier("accessTokenRequest")
        private AccessTokenRequest accessTokenRequest;

        @Bean
        @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
        public OAuth2ClientContext oauth2ClientContext() {
            return new DefaultOAuth2ClientContext(accessTokenRequest);
        }

    }

As you can see, the oauth2ClientContext bean is session-scoped, so it's serialized in Redis.

When the session is deserialized (in another microservice), there is an attempt at deserializing the DefaultOAuth2ClientContext. However, the spring-security-oauth dependency is not present in that microservice (and in my opinion, it should not, since that service doesn't care about authentication).

See the stacktrace:

org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is org.springframework.core.NestedIOException: Failed to deserialize object type; nested exception is java.lang.ClassNotFoundException: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:82) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    at org.springframework.data.redis.core.AbstractOperations.deserializeHashValue(AbstractOperations.java:338) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    at org.springframework.data.redis.core.AbstractOperations.deserializeHashMap(AbstractOperations.java:282) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    at org.springframework.data.redis.core.DefaultHashOperations.entries(DefaultHashOperations.java:227) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    at org.springframework.data.redis.core.DefaultBoundHashOperations.entries(DefaultBoundHashOperations.java:102) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.getSession(RedisOperationsSessionRepository.java:432) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.getSession(RedisOperationsSessionRepository.java:402) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.getSession(RedisOperationsSessionRepository.java:245) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:326) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:343) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:214) ~[spring-session-1.2.2.RELEASE.jar:na]
    at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:231) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.security.web.context.HttpSessionSecurityContextRepository.loadContext(HttpSessionSecurityContextRepository.java:110) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:100) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:105) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:164) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80) ~[spring-session-1.2.2.RELEASE.jar:na]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:106) ~[spring-boot-actuator-1.5.6.RELEASE.jar:1.5.6.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_131]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_131]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.16.jar:8.5.16]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is org.springframework.core.NestedIOException: Failed to deserialize object type; nested exception is java.lang.ClassNotFoundException: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
    at org.springframework.core.serializer.support.DeserializingConverter.convert(DeserializingConverter.java:78) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.core.serializer.support.DeserializingConverter.convert(DeserializingConverter.java:36) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:80) ~[spring-data-redis-1.8.6.RELEASE.jar:na]
    ... 63 common frames omitted
Caused by: org.springframework.core.NestedIOException: Failed to deserialize object type; nested exception is java.lang.ClassNotFoundException: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
    at org.springframework.core.serializer.DefaultDeserializer.deserialize(DefaultDeserializer.java:73) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.core.serializer.support.DeserializingConverter.convert(DeserializingConverter.java:73) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    ... 65 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_131]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_131]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) ~[na:1.8.0_131]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_131]
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:250) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at org.springframework.core.ConfigurableObjectInputStream.resolveClass(ConfigurableObjectInputStream.java:74) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1826) ~[na:1.8.0_131]
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713) ~[na:1.8.0_131]
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000) ~[na:1.8.0_131]
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535) ~[na:1.8.0_131]
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422) ~[na:1.8.0_131]
    at org.springframework.core.serializer.DefaultDeserializer.deserialize(DefaultDeserializer.java:70) ~[spring-core-4.3.10.RELEASE.jar:4.3.10.RELEASE]
    ... 66 common frames omitted

What's your advice in order to avoid that problem, while keeping the microservices dependencies clean?

Thx

RenaudDenis commented 6 years ago

By the way, the path toward this configuration is:

@EnableOAuth2Client => org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration => org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2ClientContextConfiguration

clarkmc commented 6 years ago

Did you ever find a solution to this? I'm also using spring-session-data-redis along with spring-security-oauth2, and I'm getting the same exception.

FrozenDroid commented 6 years ago

Yes, same problem here

EvgeniGordeev commented 3 years ago

SpringBoot 2.4.5:

First we had a serialization issue which got fixed with:

    @Bean
    public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
        FilterRegistrationBean<RequestContextFilter> registration = new FilterRegistrationBean<>(new RequestContextFilter());
        registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER - 1);
        return registration;
    }

Then we had a deserialization issue:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `jdk.proxy2.$Proxy399` (no Creators, like default constructor, exist): no default constructor found
 at [Source: (byte[])"{"@class":"org.springframework.security.oauth2.client.DefaultOAuth2ClientContext","accessToken":null,"accessTokenRequest":{"@class":"jdk.proxy2.$Proxy399"}}"; line: 1, column: 155] (through reference chain: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext["accessTokenRequest"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `jdk.proxy2.$Proxy399` (no Creators, like default constructor, exist): no default constructor found

Workaround: custom typed Jackson Deserializer. e.g.:

public class DefaultOAuth2ClientContextDeserializer extends JsonDeserializer<DefaultOAuth2ClientContext> {

    @Override
    public DefaultOAuth2ClientContext deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode jsonNode = mapper.readTree(jp);
        OAuth2AccessToken accessToken = mapper.convertValue(readJsonNode(jsonNode, "accessToken"), new TypeReference<>() {
        });

        DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext(readTokenRequest(jsonNode, mapper));
        if (accessToken != null) {
            context.setAccessToken(accessToken);
        }
        return context;
    }

    private AccessTokenRequest readTokenRequest(JsonNode jsonNode, ObjectMapper mapper) {
        ObjectNode accessTokenRequestNode = (ObjectNode) readJsonNode(jsonNode, "accessTokenRequest");
        String typeField = JsonTypeInfo.Id.CLASS.getDefaultPropertyName();

        if (accessTokenRequestNode.has(typeField)) {
            String type = accessTokenRequestNode.asText(typeField);
            try {
                Class<?> clazz = Class.forName(type);
                if (!AccessTokenRequest.class.isAssignableFrom(clazz)) {
                    throw new ClassNotFoundException();
                }
            } catch (ClassNotFoundException ignored) {
                accessTokenRequestNode.put(typeField, DefaultAccessTokenRequest.class.getName());
            }
            return (AccessTokenRequest) mapper.convertValue(accessTokenRequestNode, Object.class);
        } else {
            return null;
        }
    }

    private JsonNode readJsonNode(JsonNode jsonNode, String field) {
        return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
    }
}
EvgeniGordeev commented 3 years ago

^ It didn't work for us in the end - our logic relies on SessionRepositoryFilter being before RequestContextFilter.

AndreyShishchits commented 3 years ago

SessionRepositoryFilter relies on RequestContextHolder attributes, but RequestContextFilter resets them right before SessionRepositoryFilter tries to save current session. So I have added two more filters just to keep it simpler. Workaround recreates in reverse chain RequestContextHolder attributes in RecreateRequestContextFilter between RequestContextFilter and SessionRepositoryFilter filters and resets them in EvictRequestContextFilter after all.

public class EvictRequestContextFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            doFilter(request, response, filterChain);
        } finally {
            resetContextHolders();
            if (logger.isTraceEnabled()) {
                logger.trace("Cleared recreated thread-bound request context: " + request);
            }
        }
    }
    private void resetContextHolders() {
        LocaleContextHolder.resetLocaleContext();
        RequestContextHolder.resetRequestAttributes();
    }
}
public class RecreateRequestContextFilter extends OncePerRequestFilter {
    private boolean threadContextInheritable = false;
    public RecreateRequestContextFilter() {
    }
    public RecreateRequestContextFilter(boolean threadContextInheritable) {
        this.threadContextInheritable = threadContextInheritable;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } finally {
            ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
            initContextHolders(request, attributes);
        }
    }
    private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
        LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
        RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
        if (logger.isTraceEnabled()) {
            logger.trace("Recreate and bound request context to thread: " + request);
        }
    }
}

Filters should be registered in following order EvictRequestContextFilter -> SessionRepositoryFilter -> RecreateRequestContextFilter -> RequestContextFilter

@Bean
public FilterRegistrationBean<EvictRequestContextFilter> evictRequestContextFilter() {
    FilterRegistrationBean<EvictRequestContextFilter> registration = new FilterRegistrationBean<>(new EvictRequestContextFilter());
    registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER - 1);
    return registration;
}
@Bean
public FilterRegistrationBean<RecreateRequestContextFilter> recreateRequestContextFilter() {
    FilterRegistrationBean<RecreateRequestContextFilter> registration = new FilterRegistrationBean<>(new RecreateRequestContextFilter());
    registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER + 1);
    return registration;
}
@Bean
public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
    FilterRegistrationBean<RequestContextFilter> registration = new FilterRegistrationBean<>(new RequestContextFilter());
    registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER + 2);
    return registration;
}
johnburbridge commented 2 years ago

Same problem here. I haven't tried the workaround yet, just wanted to lend my "ping" to this thread.