Search code examples
javaspring-bootspring-securityspring-ldapspring-security-ldap

Can't catch LDAP CommunicationException with Spring handlers


Currently I'm using ActiveDirectoryLdapAuthenticationProvider and one custom provider on security authentication process. Assuming that CustomAuthenticationProvider throws exception, security flow goes to ldapAuthenticationProvider. I have a problem that when there is no connection to AD server I get CommunicationException, which can't be handled:

a) by @ControllerAdvice and @ExceptionHandler

b) by AuthenticationFailureHandler

c) by AuthenticationEntryPoint

I got exception in console and callback for default spring "error". I'm assuming it is done somewhere in the background, but I can't do anything about it and find the cause. I want to catch CommunicationException and throw my CustomException (or any other) to resolve message on javascript client. How can I do that?

callbackUrl

SecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
            .and().authorizeRequests()
            .antMatchers("...").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().loginPage("/login")
            .failureHandler(new CustomFailureHandler())
            .successHandler(new CustomSuccessHandler()).permitAll()
            .and().logout().permitAll()
            .and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth
            .authenticationProvider(new CustomAuthenticationProvider())
            .authenticationProvider(ldapAuthenticationProvider());
}

UnauthorizedHandler:

@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException authException) throws IOException {
        String[] url = request.getRequestURL().toString().split(request.getContextPath());
        String redirect = request.getContextPath();
        if (url.length == 2) redirect = request.getContextPath() + "/?callbackUrl=" + url[1].substring(1);
        response.sendRedirect(redirectUrl);
    }
}

CustomFailureHandler:

@Component
public class CustomFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        AuthenticationException exception) throws IOException {
        String message = "...";

        if (exception instanceof BadCredentialsException) {
            message = "...";
        } else if (exception instanceof CommunicationException) {
            message = "...";
        }

        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setStatus(500);
        httpServletResponse.getWriter().print(message);
        httpServletResponse.getWriter().flush();
        httpServletResponse.getWriter().close();
    }
}

Exception:

org.springframework.ldap.CommunicationException: 10.1.3.13:389; nested exception is javax.naming.CommunicationException: 10.1.3.13:389 [Root exception is java.net.ConnectException: Connection timed out: connect]
    at org.springframework.ldap.support.LdapUtils.convertLdapException(LdapUtils.java:108)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.bindAsUser(ActiveDirectoryLdapAuthenticationProvider.java:215)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.doAuthentication(ActiveDirectoryLdapAuthenticationProvider.java:146)
    at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:85)
    at org........authenticate(ActiveDirectoryLdapAuthenticationProvider.java:29)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:200)
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:124)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1417)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:835)
Caused by: javax.naming.CommunicationException: 10.1.3.13:389
    at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:237)
    at java.naming/com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
    at java.naming/com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1610)
    at java.naming/com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2752)
    at java.naming/com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:320)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxFromUrl(LdapCtxFactory.java:225)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:189)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:243)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:154)
    at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:84)
    at java.naming/javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:730)
    at java.naming/javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305)
    at java.naming/javax.naming.InitialContext.init(InitialContext.java:236)
    at java.naming/javax.naming.ldap.InitialLdapContext.<init>(InitialLdapContext.java:154)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider$ContextFactory.createContext(ActiveDirectoryLdapAuthenticationProvider.java:426)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.bindAsUser(ActiveDirectoryLdapAuthenticationProvider.java:206)
    ... 47 common frames omitted
Caused by: java.net.ConnectException: Connection timed out: connect
    at java.base/java.net.PlainSocketImpl.connect0(Native Method)
    at java.base/java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:101)
    at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399)
    at java.base/java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:242)
    at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:224)
    at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)
    at java.base/java.net.Socket.connect(Socket.java:591)
    at java.base/java.net.Socket.connect(Socket.java:540)
    at java.base/java.net.Socket.<init>(Socket.java:436)
    at java.base/java.net.Socket.<init>(Socket.java:213)
    at java.naming/com.sun.jndi.ldap.Connection.createSocket(Connection.java:330)
    at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:216)
    ... 62 common frames omitted

Solution

  • CommunicationException is not an instance of AuthenticationException so the line

    if (exception instanceof CommunicationException) {
    

    will never pass.

    A simple way to catch this exception would be to wrap the LDAP authentication provider in your own:

    public class MyLdapAuthenticationProvider implements AuthenticationProvider {
        private final ActiveDirectoryLdapAuthenticationProvider delegate;
    
        public MyLdapAuthenticationProvider(
            ActiveDirectoryLdapAuthenticationProvider delegate) {
            this.delegate = delegate;
        }
    
        public Authentication authenticate(Authentication authentication) {
            try {
                return this.delegate.authenticate(authentication);
            } catch (CommunicationException e) {
                throw new InternalAuthenticationServiceException(e);
            }
        }
    }
    

    And then:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth
            .authenticationProvider(new CustomAuthenticationProvider())
            .authenticationProvider(
                new MyLdapAuthenticationProvider(ldapAuthenticationProvider()));
    }
    

    Now that said, there is an open ticket in Spring Security so that CommunicationException is correctly wrapped in an InternalAuthenticationServiceException. You might consider contributing a fix so that you don't need the delegating authentication provider.