Spring Boot with Session/Redis Serialization Error with Bad Active Directory Ldap Credentials

26,472

Solution 1

I figured out a solution to this problem. I am open to any suggestions to improve the answer.

The solution is not complete as I need to look specifically for the com.sun.jndi.ldap.LdapCtx type when serialization fails so I can deal with that specific case and throw the SerializationException in all others. But I thought the general idea might be useful to anyone who is blocked on this.

Now when invalid credentials are used (eg Bad Username or Incorrect Password) the application returns to the log in page rather than blowing up :)

I added some RedisConfiguration to replace the RedisTemplate Spring Session is using.

import com.gateway.utils.LdapFailAwareRedisObjectSerializer;

@Configuration
public class RedisConfiguration {

  @Primary
  @Bean
  public RedisTemplate<String,ExpiringSession> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();

    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(new LdapFailAwareRedisObjectSerializer());

    template.setConnectionFactory(connectionFactory);
    return template;
  }
}

Here is my implementation of RedisSerializer<Object> (LdapFailAwareRedisObjectSerializer which is got from here)

public class LdapFailAwareRedisObjectSerializer implements RedisSerializer<Object> {

  private Converter<Object, byte[]> serializer = new SerializingConverter();
  private Converter<byte[], Object> deserializer = new DeserializingConverter();

  static final byte[] EMPTY_ARRAY = new byte[0];

  public Object deserialize(byte[] bytes) {
    if (isEmpty(bytes)) {
      return null;
    }

    try {
      return deserializer.convert(bytes);
    } catch (Exception ex) {
      throw new SerializationException("Cannot deserialize", ex);
    }
  }

  public byte[] serialize(Object object) {
    if (object == null) {
      return EMPTY_ARRAY;
    }

    try {
      return serializer.convert(object);
    } catch (Exception ex) {
      return EMPTY_ARRAY;
      //TODO add logic here to only return EMPTY_ARRAY for known conditions
      // else throw the SerializationException
      // throw new SerializationException("Cannot serialize", ex);
    }
  }

  private boolean isEmpty(byte[] data) {
    return (data == null || data.length == 0);
  }
}

Solution 2

The Java object to be cached must implement the serializable interface, because spring will serialize the object and store it in redis.

e.g. public class Store implement Serializable

The short story here is ensure you implement the serializable interface on your class.

I hope this help. Good luck.

Solution 3

This just worked fine for me after using classes of org.springframework.core.serializer.support.DeserializingConverter and org.springframework.core.serializer.support.SerializingConverter

/**
 * @author Meron Abraha 12/18/17
 */

public class CustomRedisSerializer implements RedisSerializer<Object> {

private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();

static final byte[] EMPTY_ARRAY = new byte[0];

public Object deserialize(byte[] bytes) {
    if (isEmpty(bytes)) {
        return null;
    }

    try {
        return deserializer.convert(bytes);
    } catch (Exception ex) {
        throw new SerializationException("Cannot deserialize", ex);
    }
}

public byte[] serialize(Object object) {
    if (object == null) {
        return EMPTY_ARRAY;
    }

    try {
        return serializer.convert(object);
    } catch (Exception ex) {
        return EMPTY_ARRAY;

    }
}

private boolean isEmpty(byte[] data) {
    return (data == null || data.length == 0);
}
}
Share:
26,472
Michael Kowalski
Author by

Michael Kowalski

Updated on September 10, 2020

Comments

  • Michael Kowalski
    Michael Kowalski over 3 years

    Hi I am new to Spring and Java, I am trying to implement a Gateway authentication server as described in this tutorial https://spring.io/guides/tutorials/spring-security-and-angular-js/

    I got everything working and then tried to implement the authentication against our company Ldap server. It works if I use a valid username and password. When I use invalid credentials the application errors.

    I am not at work so I don't have the exact error, but it is returning an ldap error (com.sun.jndi.ldap.LdapCtx) and Redis is trying to serialize it.

    Is there something I am missing in my configuration. From what I have read I think I should be looking for a way to wrap/extend the class and implement Serializable, but I am unsure of the least invasive way to do this with Spring Boot.

    Any help is greatly appreciated.

    Thanks,

    Mike Kowalski

    PS I have been working mostly in dynamic languages and frameworks until now (Javascript/Node, Php/Laravel)

    Here are what I think are the relevant parts of the Security configuration:

    @Configuration
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
        http
          .formLogin()
          .defaultSuccessUrl("/")
          .loginPage("/login")
          .permitAll()
          .and()
          .logout()
          .logoutSuccessUrl("/logout")
          .permitAll();
    
    
        http
          .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
        .and()
          .csrf().csrfTokenRepository(csrfTokenRepository())
        .and()
          .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
      }
    
      @Override
       protected void configure(AuthenticationManagerBuilder authManagerBuilder)          throws Exception {
        authManagerBuilder
          .authenticationProvider(activeDirectoryLdapAuthenticationProvider())
            .userDetailsService(userDetailsService());
       }
    
       @Bean
       public AuthenticationManager authenticationManager() {
          return new ProviderManager(
            Arrays.asList(activeDirectoryLdapAuthenticationProvider())
          );
       }
    
       @Bean
       public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
      ActiveDirectoryLdapAuthenticationProvider provider = new     ActiveDirectoryLdapAuthenticationProvider(
        "XXX.XXX", "ldaps://XXX.XXX:636");
          provider.setConvertSubErrorCodesToExceptions(true);
          provider.setUseAuthenticationRequestCredentials(true);
          return provider;
       }
    
      private Filter csrfHeaderFilter() {
        return new OncePerRequestFilter() {
          @Override
          protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response, 
            FilterChain filterChain
          ) throws ServletException, IOException 
          {
            CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
                .getName());
            if (csrf != null) {
              Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
              String token = csrf.getToken();
              if (cookie == null || token != null
                  && !token.equals(cookie.getValue())) {
                cookie = new Cookie("XSRF-TOKEN", token);
                cookie.setPath("/");
                response.addCookie(cookie);
              }
            }
            filterChain.doFilter(request, response);
          }
        };
      }
    
      private CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new     HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-XSRF-TOKEN");
        return repository;
      }
    
    }
    

    Here is part of the error that using invalid credentials:

    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : /login at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@44258b05
    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : /login at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : /login at position 5 of 13 in additional filter chain; firing Filter: ''
    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : /login at position 6 of 13 in additional filter chain; firing Filter: 'LogoutFilter'
    2015-09-24 15:07:30.564 DEBUG 6552 --- [nio-8080-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/logout'
    2015-09-24 15:07:30.580 DEBUG 6552 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy        : /login at position 7 of 13 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
    2015-09-24 15:07:30.580 DEBUG 6552 --- [nio-8080-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/login'; against '/login'
    2015-09-24 15:07:30.580 DEBUG 6552 --- [nio-8080-exec-3] w.a.UsernamePasswordAuthenticationFilter : Request is to process authentication
    2015-09-24 15:07:30.580 DEBUG 6552 --- [nio-8080-exec-3] o.s.s.authentication.ProviderManager     : Authentication attempt using org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider
    2015-09-24 15:07:30.580 DEBUG 6552 --- [nio-8080-exec-3] ctiveDirectoryLdapAuthenticationProvider : Processing authentication request for user: admin
    2015-09-24 15:07:31.113 DEBUG 6552 --- [nio-8080-exec-3] ctiveDirectoryLdapAuthenticationProvider : Authentication for [email protected] failed:javax.naming.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 525, vece]
    2015-09-24 15:07:31.113  INFO 6552 --- [nio-8080-exec-3] ctiveDirectoryLdapAuthenticationProvider : Active Directory authentication failed: User was not found in directory
    2015-09-24 15:07:31.114 DEBUG 6552 --- [nio-8080-exec-3] w.a.UsernamePasswordAuthenticationFilter : Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
    2015-09-24 15:07:31.114 DEBUG 6552 --- [nio-8080-exec-3] w.a.UsernamePasswordAuthenticationFilter : Updated SecurityContextHolder to contain null Authentication
    2015-09-24 15:07:31.114 DEBUG 6552 --- [nio-8080-exec-3] w.a.UsernamePasswordAuthenticationFilter : Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@28626d9a
    2015-09-24 15:07:31.114 DEBUG 6552 --- [nio-8080-exec-3] .a.SimpleUrlAuthenticationFailureHandler : Redirecting to /login?error
    2015-09-24 15:07:31.115 DEBUG 6552 --- [nio-8080-exec-3] o.s.s.web.DefaultRedirectStrategy        : Redirecting to '/login?error'
    2015-09-24 15:07:31.115 DEBUG 6552 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
    2015-09-24 15:07:31.139 DEBUG 6552 --- [nio-8080-exec-3] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
    2015-09-24 15:07:31.148 ERROR 6552 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
    
    org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx
            at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:52)
            at org.springframework.data.redis.core.AbstractOperations.rawHashValue(AbstractOperations.java:146)
            at org.springframework.data.redis.core.DefaultHashOperations.putAll(DefaultHashOperations.java:128)
            at org.springframework.data.redis.core.DefaultBoundHashOperations.putAll(DefaultBoundHashOperations.java:85)
            at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveDelta(RedisOperationsSessionRepository.java:409)
            at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.access$000(RedisOperationsSessionRepository.java:331)
            at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:211)
            at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:141)
            at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:193)
            at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:169)
            at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:127)
            at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:65)
            at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
            at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
            at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
            at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
            at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
            at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
            at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
            at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
            at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
            at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
            at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:68)
            at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
            at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
            at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
            at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
            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:142)
            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:518)
            at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091)
            at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:668)
            at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1521)
            at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1478)
            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)
    Caused by: org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx
            at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:67)
            at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:34)
            at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:50)
            ... 40 common frames omitted
    Caused by: java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx
            at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
            at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
            at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
            at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
            at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
            at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
            at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:441)
            at java.lang.Throwable.writeObject(Throwable.java:985)
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:497)
            at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:988)
            at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
            at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
            at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
            at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
            at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:44)
            at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:62)
            ... 42 common frames omitted