Search code examples
spring-securityspring-boothazelcastspring-ldapspring-session

Spring Boot Application with Hazelcast Backed Spring Session Serialization Exception on Active Directory Login Failure


We have a Spring Boot application using a Hazecast-backed Spring Session. The application authenicates with Active Directory using Spring Security. If a user attempts to log in with invalid credentials, a serialization error is thrown:

com.hazelcast.nio.serialization.HazelcastSerializationException: java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx
        at com.hazelcast.nio.serialization.SerializationServiceImpl.handleException(SerializationServiceImpl.java:380)
        at com.hazelcast.nio.serialization.SerializationServiceImpl.toData(SerializationServiceImpl.java:235)
        at com.hazelcast.nio.serialization.SerializationServiceImpl.toData(SerializationServiceImpl.java:207)
        at com.hazelcast.map.impl.MapServiceContextImpl.toData(MapServiceContextImpl.java:338)
        at com.hazelcast.map.impl.proxy.MapProxySupport.toData(MapProxySupport.java:1160)
        at com.hazelcast.map.impl.proxy.MapProxyImpl.put(MapProxyImpl.java:96)
        at org.springframework.session.hazelcast.config.annotation.web.http.HazelcastHttpSessionConfiguration$ExpiringSessionMap.put(HazelcastHttpSessionConfiguration.java:112)
        at org.springframework.session.hazelcast.config.annotation.web.http.HazelcastHttpSessionConfiguration$ExpiringSessionMap.put(HazelcastHttpSessionConfiguration.java:102)
        at org.springframework.session.MapSessionRepository.save(MapSessionRepository.java:72)
        at org.springframework.session.MapSessionRepository.save(MapSessionRepository.java:36)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:194)
        at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:170)
        at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:128)
        at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:65)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:103)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:522)
        at org.apache.coyote.ajp.AbstractAjpProcessor.process(AbstractAjpProcessor.java:868)
        at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1502)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1458)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)

This appears to be the identical to another issue (Spring Boot with Session/Redis Serialization Error with Bad Active Directory Ldap Credentials) with Redis, however there doesn't appear to be a similar mechanism to control serialization in the Hazelcast session mapping that there is for Redis in Spring Session.

We've come up with a workaround (below), but it seems less than ideal as HazelcastHttpSessionConfiguration doesn't really seem to lend itself to extension, so it seems like there should be a cleaner way that we aren't seeing.

We are extending the HazelcastHttpSessionConfiguration to get at the ExpiringSessionMap to remove the LdapCtx before serialization is attempted. This doesn't seem ideal as the HazelcastHttpSessionConfiguration doesn't really lend it self to extension, requiring duplication of code.

Is there a better solution that we're missing?

@Configuration
public class CustomHazelcastHttpSessionMapConfiguration extends HazelcastHttpSessionConfiguration{

    private String sessionMapName = "spring:session:sessions";
    private int maxInactiveIntervalInSeconds = 1800;

    @Bean
    public SessionRepository<ExpiringSession> sessionRepository(
            HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) {
        super.sessionRepository(hazelcastInstance, sessionListener);

        MapSessionRepository sessionRepository = new MapSessionRepository(
                new CustomExpiringSessionMap(hazelcastInstance.getMap(this.sessionMapName)));
        sessionRepository
                .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);

        return sessionRepository;
    }

    @Override
    public void setSessionMapName(String sessionMapName) {
        this.sessionMapName = sessionMapName;
        super.setSessionMapName(sessionMapName);
    }

    @Override
    public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
        this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
        super.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds);
    }

    static class CustomExpiringSessionMap implements Map<String, ExpiringSession> {
        private IMap<String, ExpiringSession> delegate;

        CustomExpiringSessionMap(IMap<String, ExpiringSession> delegate) {
            this.delegate = delegate;
        }

        public ExpiringSession put(String key, ExpiringSession value) {
            if (value == null) {
                return this.delegate.put(key, value);
            }
            for (String attrName : value.getAttributeNames()) {
                Object attrVal = value.getAttribute(attrName);
                // Don't serialize LdapCtx in a BadCredentialsException
                if (attrVal instanceof BadCredentialsException &&
                        ((BadCredentialsException) attrVal).getCause() != null &&
                        ((BadCredentialsException) attrVal).getCause() instanceof ActiveDirectoryAuthenticationException &&
                        ((BadCredentialsException) attrVal).getCause().getCause() != null &&
                        ((BadCredentialsException) attrVal).getCause().getCause() instanceof javax.naming.AuthenticationException) {
                    ((javax.naming.AuthenticationException) ((BadCredentialsException) attrVal).getCause().getCause()).setResolvedObj(null);
                }
            }
            return this.delegate.put(key, value, value.getMaxInactiveIntervalInSeconds(),
                    TimeUnit.SECONDS);
        }

    /*... copy and paste of the rest of ExpiringSessionMap */
    }
}

Solution

  • You should configure a custom serialization for object(s) you're having issues with.

    This way you would address your problem in Hazelcast configuration without extending/duplicating Spring Session's Hazelcast configuration.