Repository: gravitee-io/graviteeio-access-management
Pull request #659 (2019-10-10 14:53:40)

feat(ldap): manage pooled connexions for LDAP Identity Provider
closes gravitee-io/issues#2691


Patch:
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/auth/idp/impl/IdentityProviderManagerImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/auth/idp/impl/IdentityProviderManagerImpl.java
index 8e2a5887da..53f90143ff 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/auth/idp/impl/IdentityProviderManagerImpl.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-common/src/main/java/io/gravitee/am/gateway/handler/common/auth/idp/impl/IdentityProviderManagerImpl.java
@@ -110,6 +110,7 @@ public class IdentityProviderManagerImpl extends AbstractService implements Iden
 
         logger.info("Dispose event listener for identity provider events for domain {}", domain.getName());
         eventManager.unsubscribeForEvents(this, IdentityProviderEvent.class, domain.getId());
+        clearProviders();
     }
 
     @Override
@@ -142,22 +143,52 @@ public class IdentityProviderManagerImpl extends AbstractService implements Iden
 
     private void removeIdentityProvider(String identityProviderId) {
         logger.info("Domain {} has received identity provider event, delete identity provider {}", domain.getName(), identityProviderId);
-        providers.remove(identityProviderId);
-        identities.remove(identityProviderId);
-        userProviders.remove(identityProviderId);
+        clearProvider(identityProviderId);
     }
 
     private void updateAuthenticationProvider(IdentityProvider identityProvider) {
         logger.info("\tInitializing identity provider: {} [{}]", identityProvider.getName(), identityProvider.getType());
-        AuthenticationProvider authenticationProvider =
-                identityProviderPluginManager.create(identityProvider.getType(), identityProvider.getConfiguration(),
-                        identityProvider.getMappers(), identityProvider.getRoleMapper());
-        UserProvider userProvider =
-                identityProviderPluginManager.create(identityProvider.getType(), identityProvider.getConfiguration());
-        providers.put(identityProvider.getId(), authenticationProvider);
-        identities.put(identityProvider.getId(), identityProvider);
-        if (userProvider != null) {
-            userProviders.put(identityProvider.getId(), userProvider);
+        try {
+            // stop existing provider, if any
+            clearProvider(identityProvider.getId());
+            // create and start the new provider
+            AuthenticationProvider authenticationProvider =
+                    identityProviderPluginManager.create(identityProvider.getType(), identityProvider.getConfiguration(),
+                            identityProvider.getMappers(), identityProvider.getRoleMapper());
+            if (authenticationProvider != null) {
+                // start the authentication provider
+                authenticationProvider.start();
+                // init the user provider
+                UserProvider userProvider =
+                        identityProviderPluginManager.create(identityProvider.getType(), identityProvider.getConfiguration());
+                providers.put(identityProvider.getId(), authenticationProvider);
+                identities.put(identityProvider.getId(), identityProvider);
+                if (userProvider != null) {
+                    userProviders.put(identityProvider.getId(), userProvider);
+                }
+            }
+        } catch (Exception ex) {
+            // failed to load the plugin
+            logger.error("An error occurs while initializing the identity provider : {}", identityProvider.getName(), ex);
+            clearProvider(identityProvider.getId());
+        }
+    }
+
+    private void clearProviders() {
+        providers.keySet().forEach(this::clearProvider);
+    }
+
+    private void clearProvider(String identityProviderId) {
+        AuthenticationProvider authenticationProvider = providers.remove(identityProviderId);
+        if (authenticationProvider != null) {
+            // stop the authentication provider
+            try {
+                authenticationProvider.stop();
+            } catch (Exception e) {
+                logger.error("An error occurs while stopping the identity provider : {}", identityProviderId, e);
+            }
         }
+        identities.remove(identityProviderId);
+        userProviders.remove(identityProviderId);
     }
 }
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-api/src/main/java/io/gravitee/am/identityprovider/api/AuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-api/src/main/java/io/gravitee/am/identityprovider/api/AuthenticationProvider.java
index 0a0e3c707c..c9984ff9d9 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-api/src/main/java/io/gravitee/am/identityprovider/api/AuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-api/src/main/java/io/gravitee/am/identityprovider/api/AuthenticationProvider.java
@@ -15,6 +15,8 @@
  */
 package io.gravitee.am.identityprovider.api;
 
+import io.gravitee.common.component.Lifecycle;
+import io.gravitee.common.service.Service;
 import io.reactivex.Maybe;
 
 /**
@@ -22,10 +24,21 @@ import io.reactivex.Maybe;
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
-public interface AuthenticationProvider {
+public interface AuthenticationProvider extends Service<AuthenticationProvider> {
 
     Maybe<User> loadUserByUsername(Authentication authentication);
 
     Maybe<User> loadUserByUsername(String username);
 
+    default Lifecycle.State lifecycleState() {
+        return Lifecycle.State.INITIALIZED;
+    }
+
+    default AuthenticationProvider start() throws Exception {
+        return this;
+    }
+
+    default AuthenticationProvider stop() throws Exception {
+        return this;
+    }
 }
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/pom.xml b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/pom.xml
index a00ba331f9..08539d3dfe 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/pom.xml
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/pom.xml
@@ -35,6 +35,7 @@
     <properties>
         <json-schema-generator-maven-plugin.version>1.3.0</json-schema-generator-maven-plugin.version>
         <json-schema-generator-maven-plugin.outputDirectory>${project.build.directory}/schemas</json-schema-generator-maven-plugin.outputDirectory>
+        <ldaptive.version>1.2.4</ldaptive.version>
     </properties>
 
     <dependencies>
@@ -57,7 +58,13 @@
         <dependency>
             <groupId>org.ldaptive</groupId>
             <artifactId>ldaptive</artifactId>
-            <version>1.2.3</version>
+            <version>${ldaptive.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ldaptive</groupId>
+            <artifactId>ldaptive-unboundid</artifactId>
+            <version>${ldaptive.version}</version>
         </dependency>
 
         <!-- Unit tests -->
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/LdapIdentityProviderConfiguration.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/LdapIdentityProviderConfiguration.java
index e7b51c25d7..0d93d07d7b 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/LdapIdentityProviderConfiguration.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/LdapIdentityProviderConfiguration.java
@@ -47,6 +47,10 @@ public class LdapIdentityProviderConfiguration implements IdentityProviderConfig
 
     private Long responseTimeout = 5000l;
 
+    private Integer minPoolSize = 5;
+
+    private Integer maxPoolSize = 15;
+
     private String passwordEncoding;
 
     private boolean hashEncodedByThirdParty;
@@ -139,6 +143,22 @@ public class LdapIdentityProviderConfiguration implements IdentityProviderConfig
         this.responseTimeout = responseTimeout;
     }
 
+    public Integer getMinPoolSize() {
+        return minPoolSize;
+    }
+
+    public void setMinPoolSize(Integer minPoolSize) {
+        this.minPoolSize = minPoolSize;
+    }
+
+    public Integer getMaxPoolSize() {
+        return maxPoolSize;
+    }
+
+    public void setMaxPoolSize(Integer maxPoolSize) {
+        this.maxPoolSize = maxPoolSize;
+    }
+
     public String getPasswordAlgorithm() {
         return passwordAlgorithm;
     }
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/CompareAuthenticationHandler.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/CompareAuthenticationHandler.java
index a672b21c1a..80a0923626 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/CompareAuthenticationHandler.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/CompareAuthenticationHandler.java
@@ -19,9 +19,10 @@ import io.gravitee.am.identityprovider.ldap.LdapIdentityProviderConfiguration;
 import io.gravitee.am.identityprovider.ldap.authentication.encoding.BinaryToTextEncoder;
 import io.gravitee.am.identityprovider.ldap.authentication.encoding.PasswordEncoder;
 import org.ldaptive.*;
-import org.ldaptive.auth.AbstractCompareAuthenticationHandler;
 import org.ldaptive.auth.AuthenticationCriteria;
 import org.ldaptive.auth.AuthenticationHandlerResponse;
+import org.ldaptive.auth.PooledCompareAuthenticationHandler;
+import org.ldaptive.pool.PooledConnectionFactory;
 
 import java.util.Arrays;
 
@@ -29,20 +30,19 @@ import java.util.Arrays;
  * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
  * @author GraviteeSource Team
  */
-public class CompareAuthenticationHandler extends AbstractCompareAuthenticationHandler implements ConnectionFactoryManager {
+public class CompareAuthenticationHandler extends PooledCompareAuthenticationHandler {
 
-    private ConnectionFactory factory;
     private PasswordEncoder passwordEncoder;
     private BinaryToTextEncoder binaryToTextEncoder;
     private LdapIdentityProviderConfiguration configuration;
 
     public CompareAuthenticationHandler() {}
 
-    public CompareAuthenticationHandler(final ConnectionFactory cf) {
-        setConnectionFactory(cf);
+    public CompareAuthenticationHandler(final PooledConnectionFactory cf) {
+        super(cf);
     }
 
-    public CompareAuthenticationHandler(ConnectionFactory connectionFactory, PasswordEncoder passwordEncoder, BinaryToTextEncoder binaryToTextEncoder, LdapIdentityProviderConfiguration configuration) {
+    public CompareAuthenticationHandler(PooledConnectionFactory connectionFactory, PasswordEncoder passwordEncoder, BinaryToTextEncoder binaryToTextEncoder, LdapIdentityProviderConfiguration configuration) {
         this(connectionFactory);
         this.passwordEncoder = passwordEncoder;
         this.binaryToTextEncoder = binaryToTextEncoder;
@@ -75,24 +75,6 @@ public class CompareAuthenticationHandler extends AbstractCompareAuthenticationH
                         compareResponse.getMessageId());
     }
 
-
-    @Override
-    public ConnectionFactory getConnectionFactory() {
-        return factory;
-    }
-
-    @Override
-    public void setConnectionFactory(ConnectionFactory cf) {
-        factory = cf;
-    }
-
-    @Override
-    protected Connection getConnection() throws LdapException {
-        final Connection conn = factory.getConnection();
-        conn.open();
-        return conn;
-    }
-
     @Override
     public String toString() {
         return
@@ -100,7 +82,7 @@ public class CompareAuthenticationHandler extends AbstractCompareAuthenticationH
                         "[%s@%d::factory=%s, passwordAttribute=%s, passwordScheme=%s, controls=%s]",
                         getClass().getName(),
                         hashCode(),
-                        factory,
+                        getConnectionFactory(),
                         getPasswordAttribute(),
                         passwordEncoder.getPasswordSchemeLabel(),
                         Arrays.toString(getAuthenticationControls()));
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/GroupSearchEntryHandler.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/GroupSearchEntryHandler.java
new file mode 100644
index 0000000000..387b237fd7
--- /dev/null
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/GroupSearchEntryHandler.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.identityprovider.ldap.authentication;
+
+import org.ldaptive.*;
+import org.ldaptive.handler.HandlerResult;
+import org.ldaptive.handler.SearchEntryHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Provides post groups search handling of a search entry (i.e the authenticated user)
+ *
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+public class GroupSearchEntryHandler implements SearchEntryHandler {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(GroupSearchEntryHandler.class);
+    private static final String MEMBEROF_ATTRIBUTE = "memberOf";
+    private String baseDn;
+    private String searchFilter;
+    private SearchScope searchScope;
+    private String[] returnAttributes;
+
+    @Override
+    public HandlerResult<SearchEntry> handle(Connection conn, SearchRequest request, SearchEntry entry) throws LdapException {
+        try {
+            final SearchFilter filter = new SearchFilter();
+            filter.setFilter(searchFilter);
+            filter.setParameter(0, entry.getDn());
+
+            final SearchRequest searchRequest = new SearchRequest();
+            searchRequest.setBaseDn(baseDn);
+            searchRequest.setReturnAttributes(returnAttributes);
+            searchRequest.setSearchScope(searchScope);
+            searchRequest.setSearchFilter(filter);
+
+            final SearchOperation searchOperation = new SearchOperation(conn);
+            SearchResult searchResult = searchOperation.execute(searchRequest).getResult();
+
+            // update search entry
+            Collection<LdapEntry> groupEntries = searchResult.getEntries();
+            String[] groups = groupEntries.stream()
+                    .map(groupEntry -> groupEntry.getAttributes()
+                            .stream()
+                            .map(ldapAttribute -> ldapAttribute.getStringValue())
+                            .collect(Collectors.toList()))
+                    .flatMap(List::stream)
+                    .toArray(size -> new String[size]);
+            entry.addAttribute(new LdapAttribute(MEMBEROF_ATTRIBUTE, groups));
+        } catch (Exception ex) {
+            LOGGER.warn("No group found for user {}", entry.getDn(), ex);
+        } finally {
+            return new HandlerResult<>(entry);
+        }
+    }
+
+    @Override
+    public void initializeRequest(SearchRequest request) {
+        // nothing to do
+    }
+
+    public void setBaseDn(String baseDn) {
+        this.baseDn = baseDn;
+    }
+
+    public void setSearchFilter(String searchFilter) {
+        this.searchFilter = searchFilter;
+    }
+
+    public void setSearchScope(SearchScope searchScope) {
+        this.searchScope = searchScope;
+    }
+
+    public void setReturnAttributes(String[] returnAttributes) {
+        this.returnAttributes = returnAttributes;
+    }
+}
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
index afc612d8ce..d144ecf3d8 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProvider.java
@@ -27,12 +27,13 @@ import io.gravitee.am.identityprovider.ldap.authentication.spring.LdapAuthentica
 import io.gravitee.am.service.exception.authentication.BadCredentialsException;
 import io.gravitee.am.service.exception.authentication.InternalAuthenticationServiceException;
 import io.gravitee.am.service.exception.authentication.UsernameNotFoundException;
+import io.gravitee.common.service.AbstractService;
 import io.reactivex.Maybe;
 import org.ldaptive.*;
 import org.ldaptive.auth.AuthenticationRequest;
 import org.ldaptive.auth.AuthenticationResponse;
 import org.ldaptive.auth.Authenticator;
-import org.ldaptive.auth.SearchDnResolver;
+import org.ldaptive.pool.ConnectionPool;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.InitializingBean;
@@ -41,7 +42,6 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.Import;
 
 import java.util.*;
-import java.util.stream.Collectors;
 
 /**
  * @author David BRASSELY (david.brassely at graviteesource.com)
@@ -49,7 +49,7 @@ import java.util.stream.Collectors;
  * @author GraviteeSource Team
  */
 @Import(LdapAuthenticationProviderConfiguration.class)
-public class LdapAuthenticationProvider implements AuthenticationProvider, InitializingBean {
+public class LdapAuthenticationProvider extends AbstractService<AuthenticationProvider> implements AuthenticationProvider, InitializingBean {
 
     private final Logger LOGGER = LoggerFactory.getLogger(LdapAuthenticationProvider.class);
 
@@ -70,11 +70,13 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Initi
     private Authenticator authenticator;
 
     @Autowired
-    private ConnectionFactory connectionFactory;
+    private ConnectionFactory searchConnectionFactory;
 
     @Autowired
-    @Qualifier("groupSearchExecutor")
-    private SearchExecutor groupSearchExecutor;
+    private ConnectionPool bindConnectionPool;
+
+    @Autowired
+    private ConnectionPool searchConnectionPool;
 
     @Autowired
     @Qualifier("userSearchExecutor")
@@ -97,77 +99,75 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Initi
         LOGGER.info("User identifier is based on the [{}] attribute", identifierAttribute);
     }
 
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        LOGGER.info("Init LDAP {} connection pools", configuration.getContextSourceUrl());
+        if (bindConnectionPool != null) {
+            bindConnectionPool.initialize();
+        }
+        if (searchConnectionPool != null) {
+            searchConnectionPool.initialize();
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+
+        LOGGER.info("Close LDAP {} connection pools", configuration.getContextSourceUrl());
+        if (bindConnectionPool != null) {
+            bindConnectionPool.close();
+        }
+        if (searchConnectionPool != null) {
+            searchConnectionPool.close();
+        }
+    }
+
     @Override
     public Maybe<User> loadUserByUsername(Authentication authentication) {
-        Maybe<User> userSource = Maybe.create(emitter -> {
+        return Maybe.fromCallable(() -> {
             try {
                 String username = (String) authentication.getPrincipal();
                 String password = (String) authentication.getCredentials();
-                // authenticate user
-                // unable *={0} authentication filter (ldaptive use *={user})
-                ((SearchDnResolver) authenticator.getDnResolver()).setUserFilterParameters(Collections.singleton(username).toArray());
-                AuthenticationResponse response = authenticator.authenticate(
-                        new AuthenticationRequest(username, new Credential(password), ReturnAttributes.ALL_USER.value()));
+                // authenticate user and and fetch groups if exist
+                AuthenticationResponse response = authenticator.authenticate(new AuthenticationRequest(username, new Credential(password), ReturnAttributes.ALL_USER.value()));
                 if (response.getResult()) { // authentication succeeded
                     LdapEntry userEntry = response.getLdapEntry();
-                    // fetch user groups
-                    fetchUserGroups(userEntry);
-                    // return user
-                    emitter.onSuccess(createUser(userEntry));
+                    return userEntry;
                 } else { // authentication failed
                     LOGGER.debug("Failed to authenticate user", response.getMessage());
-                    emitter.onError(new BadCredentialsException(response.getMessage()));
+                    throw new BadCredentialsException(response.getMessage());
                 }
             } catch (LdapException e) {
                 LOGGER.error("An error occurs during LDAP authentication", e);
-                emitter.onError(new InternalAuthenticationServiceException(e.getMessage(), e));
+                throw new InternalAuthenticationServiceException(e.getMessage(), e);
             }
-        });
-
-        return userSource;
+        })
+        .map(this::createUser);
     }
 
     @Override
     public Maybe<User> loadUserByUsername(String username) {
-        Maybe<User> userSource = Maybe.create(emitter -> {
+        return Maybe.fromCallable(() -> {
             try {
                 // find user
-                userSearchExecutor.getSearchFilter().setParameter(0, username);
-                SearchResult userSearchResult = userSearchExecutor.search(connectionFactory).getResult();
+                SearchFilter searchFilter = createSearchFilter(userSearchExecutor, username);
+                SearchResult userSearchResult = userSearchExecutor.search(searchConnectionFactory, searchFilter).getResult();
                 LdapEntry userEntry = userSearchResult.getEntry();
                 if (userEntry != null) {
-                    // fetch user groups
-                    fetchUserGroups(userEntry);
-                    // return user
-                    emitter.onSuccess(createUser(userEntry));
+                    return userEntry;
                 } else { // failed to find user
-                    emitter.onError(new UsernameNotFoundException(username));
+                    throw new UsernameNotFoundException(username);
                 }
             } catch (LdapException e) {
                 LOGGER.error("An error occurs while searching for a LDAP user", e);
-                emitter.onError(new InternalAuthenticationServiceException(e.getMessage(), e));
+                throw new InternalAuthenticationServiceException(e.getMessage(), e);
             }
-        });
-
-        return userSource;
-    }
+        })
+        .map(this::createUser);
 
-    private void fetchUserGroups(LdapEntry userEntry) {
-        try {
-            groupSearchExecutor.getSearchFilter().setParameter(0, userEntry.getDn());
-            SearchResult searchResult = groupSearchExecutor.search(connectionFactory).getResult();
-            Collection<LdapEntry> groupEntries = searchResult.getEntries();
-            String[] groups = groupEntries.stream()
-                    .map(groupEntry -> groupEntry.getAttributes()
-                            .stream()
-                            .map(ldapAttribute -> ldapAttribute.getStringValue())
-                            .collect(Collectors.toList()))
-                    .flatMap(List::stream)
-                    .toArray(size -> new String[size]);
-            userEntry.addAttribute(new LdapAttribute(MEMBEROF_ATTRIBUTE, groups));
-        } catch (Exception e) {
-            LOGGER.warn("No group found for user {}", userEntry.getDn(), e);
-        }
     }
 
     private User createUser(LdapEntry ldapEntry) {
@@ -240,4 +240,21 @@ public class LdapAuthenticationProvider implements AuthenticationProvider, Initi
         }
         return new ArrayList<>(roles);
     }
+
+    /**
+     * Constructs a new search filter using {@link SearchExecutor} as a template and
+     * the username as a parameter.
+     *
+     * @param executor the executor
+     * @param username the username
+     * @return  Search filter with parameters applied.
+     */
+    private SearchFilter createSearchFilter(final SearchExecutor executor, final String username) {
+        final SearchFilter filter = new SearchFilter();
+        filter.setFilter(executor.getSearchFilter().getFilter());
+        filter.setParameter(0, username);
+
+        LOGGER.debug("Constructed LDAP search filter [{}]", filter.format());
+        return filter;
+    }
 }
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/spring/LdapAuthenticationProviderConfiguration.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/spring/LdapAuthenticationProviderConfiguration.java
index c36a860252..b312980c09 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/spring/LdapAuthenticationProviderConfiguration.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/java/io/gravitee/am/identityprovider/ldap/authentication/spring/LdapAuthenticationProviderConfiguration.java
@@ -17,14 +17,13 @@ package io.gravitee.am.identityprovider.ldap.authentication.spring;
 
 import io.gravitee.am.identityprovider.ldap.LdapIdentityProviderConfiguration;
 import io.gravitee.am.identityprovider.ldap.authentication.CompareAuthenticationHandler;
+import io.gravitee.am.identityprovider.ldap.authentication.GroupSearchEntryHandler;
 import io.gravitee.am.identityprovider.ldap.authentication.encoding.*;
 import org.ldaptive.*;
-import org.ldaptive.auth.AbstractAuthenticationHandler;
-import org.ldaptive.auth.Authenticator;
-import org.ldaptive.auth.BindAuthenticationHandler;
-import org.ldaptive.auth.SearchDnResolver;
-import org.ldaptive.auth.ext.PasswordPolicyAuthenticationResponseHandler;
-import org.ldaptive.control.PasswordPolicyControl;
+import org.ldaptive.auth.*;
+import org.ldaptive.handler.SearchEntryHandler;
+import org.ldaptive.pool.*;
+import org.ldaptive.provider.unboundid.UnboundIDProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -45,9 +44,66 @@ public class LdapAuthenticationProviderConfiguration {
     @Autowired
     private LdapIdentityProviderConfiguration configuration;
 
+    /**
+     * Bind OPERATIONS configuration
+     * We must split the connections for search and bind operations because bind operation change the connection context to the last user authenticated
+     * If the user has no role to search in the LDAP/AD directory the authentication will failed for the next users
+     */
+
+    @Bean
+    public ConnectionPool bindConnectionPool() {
+        PoolConfig poolConfig = new PoolConfig();
+        poolConfig.setMinPoolSize(configuration.getMinPoolSize());
+        poolConfig.setMaxPoolSize(configuration.getMaxPoolSize());
+        poolConfig.setValidatePeriodically(true);
+        BlockingConnectionPool connectionPool = new BlockingConnectionPool(poolConfig, (DefaultConnectionFactory) bindConnectionFactory());
+        connectionPool.setValidator(new SearchValidator());
+        return connectionPool;
+    }
+
+    @Bean
+    public PooledConnectionFactory bindPooledConnectionFactory() {
+        return new PooledConnectionFactory(bindConnectionPool());
+    }
+
+    @Bean
+    public ConnectionFactory bindConnectionFactory() {
+        UnboundIDProvider unboundIDProvider = new UnboundIDProvider();
+        DefaultConnectionFactory connectionFactory = new DefaultConnectionFactory();
+        connectionFactory.setConnectionConfig(connectionConfig());
+        connectionFactory.setProvider(unboundIDProvider);
+        return connectionFactory;
+    }
+
+    /**
+     * Search OPERATIONS configuration
+     * We must split the connections for search and bind operations because bind operation change the connection context to the last user authenticated
+     * If the user has no role to search in the LDAP/AD directory the authentication will failed for the next users
+     */
+
+    @Bean
+    public ConnectionPool searchConnectionPool() {
+        PoolConfig poolConfig = new PoolConfig();
+        poolConfig.setMinPoolSize(configuration.getMinPoolSize());
+        poolConfig.setMaxPoolSize(configuration.getMaxPoolSize());
+        poolConfig.setValidatePeriodically(true);
+        BlockingConnectionPool connectionPool = new BlockingConnectionPool(poolConfig, (DefaultConnectionFactory) searchConnectionFactory());
+        connectionPool.setValidator(new SearchValidator());
+        return connectionPool;
+    }
+
     @Bean
-    public ConnectionFactory connectionFactory() {
-        return new DefaultConnectionFactory(connectionConfig());
+    public PooledConnectionFactory searchPooledConnectionFactory() {
+        return new PooledConnectionFactory(searchConnectionPool());
+    }
+
+    @Bean
+    public ConnectionFactory searchConnectionFactory() {
+        UnboundIDProvider unboundIDProvider = new UnboundIDProvider();
+        DefaultConnectionFactory connectionFactory = new DefaultConnectionFactory();
+        connectionFactory.setConnectionConfig(connectionConfig());
+        connectionFactory.setProvider(unboundIDProvider);
+        return connectionFactory;
     }
 
     @Bean
@@ -71,38 +127,34 @@ public class LdapAuthenticationProviderConfiguration {
             searchExecutor.setBaseDn(userSearchBase + LDAP_SEPARATOR + searchExecutor.getBaseDn());
         }
         searchExecutor.setSearchFilter(new SearchFilter(configuration.getUserSearchFilter()));
-        return searchExecutor;
-    }
-
-    @Bean("groupSearchExecutor")
-    public SearchExecutor groupSearchExecutor() {
-        SearchExecutor searchExecutor = new SearchExecutor();
-        searchExecutor.setBaseDn(configuration.getContextSourceBase());
-        String groupSearchBase = configuration.getGroupSearchBase();
-        if (groupSearchBase != null && !groupSearchBase.isEmpty()) {
-            searchExecutor.setBaseDn(groupSearchBase + LDAP_SEPARATOR + searchExecutor.getBaseDn());
-        }
-        searchExecutor.setSearchFilter(new SearchFilter(configuration.getGroupSearchFilter()));
-        searchExecutor.setReturnAttributes(new String[] { configuration.getGroupRoleAttribute() });
-        searchExecutor.setSearchScope(SearchScope.SUBTREE);
+        searchExecutor.setSearchEntryHandlers(groupSearchEntryHandler());
         return searchExecutor;
     }
 
     @Bean
     public Authenticator authenticator() {
-        SearchDnResolver dnResolver = new SearchDnResolver(connectionFactory());
+        PooledSearchDnResolver dnResolver = new PooledSearchDnResolver(searchPooledConnectionFactory());
+        String userSearchBase = configuration.getUserSearchBase();
         dnResolver.setBaseDn(configuration.getContextSourceBase());
-        dnResolver.setUserFilter(configuration.getUserSearchFilter());
+        if (userSearchBase != null && !userSearchBase.isEmpty()) {
+            dnResolver.setBaseDn(userSearchBase + LDAP_SEPARATOR + dnResolver.getBaseDn());
+        }
+        // unable *={0} authentication filter (ldaptive use *={user})
+        dnResolver.setUserFilter(configuration.getUserSearchFilter().replaceAll("\\{0\\}", "{user}"));
         dnResolver.setSubtreeSearch(true);
+        dnResolver.setAllowMultipleDns(false);
 
         AbstractAuthenticationHandler authHandler =
                 (configuration.getPasswordAlgorithm() == null)
-                        ? new BindAuthenticationHandler(connectionFactory())
-                        : new CompareAuthenticationHandler(connectionFactory(), passwordEncoder(configuration.getPasswordAlgorithm()), binaryToTextEncoder(), configuration);
+                        ? new PooledBindAuthenticationHandler(bindPooledConnectionFactory())
+                        : new CompareAuthenticationHandler(searchPooledConnectionFactory(), passwordEncoder(configuration.getPasswordAlgorithm()), binaryToTextEncoder(), configuration);
+
+
+        PooledSearchEntryResolver pooledSearchEntryResolver = new PooledSearchEntryResolver(searchPooledConnectionFactory());
+        pooledSearchEntryResolver.setSearchEntryHandlers(groupSearchEntryHandler());
 
-        authHandler.setAuthenticationControls(new PasswordPolicyControl());
         Authenticator auth = new Authenticator(dnResolver, authHandler);
-        auth.setAuthenticationResponseHandlers(new PasswordPolicyAuthenticationResponseHandler());
+        auth.setEntryResolver(pooledSearchEntryResolver);
         return auth;
     }
 
@@ -119,6 +171,21 @@ public class LdapAuthenticationProviderConfiguration {
         return new NoneEncoder();
     }
 
+    private SearchEntryHandler groupSearchEntryHandler() {
+        GroupSearchEntryHandler groupSearchEntryHandler = new GroupSearchEntryHandler();
+        String groupSearchBase = configuration.getGroupSearchBase();
+        if (groupSearchBase != null && !groupSearchBase.isEmpty()) {
+            groupSearchEntryHandler.setBaseDn(groupSearchBase + LDAP_SEPARATOR + configuration.getContextSourceBase());
+        } else {
+            groupSearchEntryHandler.setBaseDn(configuration.getContextSourceBase());
+        }
+        groupSearchEntryHandler.setSearchFilter(configuration.getGroupSearchFilter());
+        groupSearchEntryHandler.setReturnAttributes(new String[] { configuration.getGroupRoleAttribute() });
+        groupSearchEntryHandler.setSearchScope(SearchScope.SUBTREE);
+
+        return groupSearchEntryHandler;
+    }
+
     private PasswordEncoder passwordEncoder(String passwordAlgorithm) {
         if ("MD5".equals(passwordAlgorithm)) {
             return new MD5PasswordEncoder();
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/resources/schemas/schema-form.json b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/resources/schemas/schema-form.json
index e1f96a7c3b..096f192738 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/resources/schemas/schema-form.json
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/main/resources/schemas/schema-form.json
@@ -64,6 +64,20 @@
       "title": "Response timeout",
       "description": "Duration of time in milliseconds to wait for responses. (default 5000 ms)"
     },
+    "minPoolSize" : {
+      "type" : "integer",
+      "default": 5,
+      "minimum": 0,
+      "title": "Min pool size",
+      "description": "Minimum pool of connections to be initialized"
+    },
+    "maxPoolSize" : {
+      "type" : "integer",
+      "default": 15,
+      "minimum": 0,
+      "title": "Max pool size",
+      "description": "Maximum pool of connections can grow to"
+    },
     "passwordAlgorithm" : {
       "type" : "string",
       "title": "Password encoder algorithm",
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/security/impl/IdentityProviderManagerImpl.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/security/impl/IdentityProviderManagerImpl.java
index b91203c6ea..9076951f67 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/security/impl/IdentityProviderManagerImpl.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-admin/src/main/java/io/gravitee/am/management/handlers/admin/security/impl/IdentityProviderManagerImpl.java
@@ -74,8 +74,16 @@ public class IdentityProviderManagerImpl implements IdentityProviderManager, Ini
             AuthenticationProvider authenticationProvider =
                     identityProviderPluginManager.create(identityProvider.getType(), identityProvider.getConfiguration(),
                             identityProvider.getMappers(), identityProvider.getRoleMapper());
-            providers.put(identityProvider.getId(), authenticationProvider);
-            identities.put(identityProvider.getId(), identityProvider);
+            // start the authentication provider
+            if (authenticationProvider != null) {
+                try {
+                    authenticationProvider.start();
+                    providers.put(identityProvider.getId(), authenticationProvider);
+                    identities.put(identityProvider.getId(), identityProvider);
+                } catch (Exception ex) {
+                    logger.error("An error occurs while starting the {} identity provider", identityProvider, ex);
+                }
+            }
         });
     }
 }


Test patch:
diff --git a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/test/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProviderTest.java b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/test/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProviderTest.java
index 7a3bda958c..0c56615fa9 100644
--- a/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/test/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProviderTest.java
+++ b/gravitee-am-identityprovider/gravitee-am-identityprovider-ldap/src/test/java/io/gravitee/am/identityprovider/ldap/authentication/LdapAuthenticationProviderTest.java
@@ -18,12 +18,14 @@ package io.gravitee.am.identityprovider.ldap.authentication;
 import io.gravitee.am.identityprovider.api.Authentication;
 import io.gravitee.am.identityprovider.api.AuthenticationProvider;
 import io.gravitee.am.identityprovider.api.User;
-import io.gravitee.am.identityprovider.ldap.LdapIdentityProviderConfiguration;
 import io.gravitee.am.service.exception.authentication.BadCredentialsException;
 import io.reactivex.observers.TestObserver;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.ldaptive.pool.ConnectionPool;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.zapodot.junit.ldap.EmbeddedLdapRule;
@@ -42,6 +44,12 @@ public abstract class LdapAuthenticationProviderTest {
     @Autowired
     protected AuthenticationProvider authenticationProvider;
 
+    @Autowired
+    private ConnectionPool bindConnectionPool;
+
+    @Autowired
+    private ConnectionPool searchConnectionPool;
+
     @Rule
     public EmbeddedLdapRule embeddedLdapRule = EmbeddedLdapRuleBuilder
             .newInstance()
@@ -51,6 +59,18 @@ public abstract class LdapAuthenticationProviderTest {
             .importingLdifs("test-server.ldif")
             .build();
 
+    @Before
+    public void init() {
+        bindConnectionPool.initialize();
+        searchConnectionPool.initialize();
+    }
+
+    @After
+    public void close() {
+        bindConnectionPool.close();
+        searchConnectionPool.close();
+    }
+
     @Test
     public void shouldLoadUserByUsername_authentication_badCredentials() throws Exception {
         embeddedLdapRule.ldapConnection();