You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/01/19 09:58:56 UTC

[4/4] mina-sshd git commit: Added a few improvements + documentation for the LDAP based authenticators

Added a few improvements + documentation for the LDAP based authenticators


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/c66c6d42
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/c66c6d42
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/c66c6d42

Branch: refs/heads/master
Commit: c66c6d421b185a86ad2851424cc84197eb8545ca
Parents: e6991a7
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Jan 19 10:58:37 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Jan 19 10:58:37 2016 +0200

----------------------------------------------------------------------
 .../common/util/net/LdapNetworkConnector.java   | 124 +++++++++++------
 .../sshd/server/auth/LdapAuthenticator.java     |  37 +++++
 .../password/LdapPasswordAuthenticator.java     |  28 +++-
 .../auth/pubkey/LdapPublickeyAuthenticator.java | 136 +++++++++++++++++--
 .../sshd/server/auth/BaseAuthenticatorTest.java |  23 ++--
 .../password/LdapPasswordAuthenticatorTest.java |   5 +-
 .../pubkey/LdapPublickeyAuthenticatorTest.java  |   5 +-
 7 files changed, 291 insertions(+), 67 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
index 1e4ff23..3bb626d 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
@@ -47,9 +47,11 @@ import org.apache.sshd.common.util.buffer.BufferUtils;
 /**
  * Uses the <A HREF="http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html">
  * LDAP Naming Service Provider for the Java Naming and Directory Interface (JNDI)</A>
+ *
+ * @param <C> Type of context being passed to {@link #resolveAttributes(String, String, Object)}
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapNetworkConnector extends NetworkConnector {
+public class LdapNetworkConnector<C> extends NetworkConnector {
     public static final String DEFAULT_LDAP_PROTOCOL = "ldap";
     public static final int DEFAULT_LDAP_PORT = 389;
 
@@ -105,6 +107,22 @@ public class LdapNetworkConnector extends NetworkConnector {
         setBinaryAttributes(DEFAULT_BINARY_ATTRIBUTES);
     }
 
+    @Override
+    public void setConnectTimeout(long connectTimeout) {
+        // value must fit in an integer
+        ValidateUtils.checkTrue((connectTimeout >= Integer.MIN_VALUE) && (connectTimeout <= Integer.MAX_VALUE), "Invalid connect timeout: %d", connectTimeout);
+        ldapEnv.put("com.sun.jndi.ldap.connect.timeout", Long.toString(connectTimeout));
+        super.setConnectTimeout(connectTimeout);
+    }
+
+    @Override
+    public void setReadTimeout(long readTimeout) {
+        // value must fit in an integer
+        ValidateUtils.checkTrue((readTimeout >= Integer.MIN_VALUE) && (readTimeout <= Integer.MAX_VALUE), "Invalid read timeout: %d", readTimeout);
+        super.setReadTimeout(readTimeout);
+        ldapEnv.put("com.sun.jndi.ldap.read.timeout", Long.toString(readTimeout));
+    }
+
     public String getLdapFactory() {
         return Objects.toString(ldapEnv.get(Context.INITIAL_CONTEXT_FACTORY), null);
     }
@@ -319,54 +337,82 @@ public class LdapNetworkConnector extends NetworkConnector {
      *                 may be {@code null}/empty if not required for the specific query
      * @param password Password Password to be used if necessary - may be {@code null}/empty if not
      *                 required for the specific query
-     * @param queryContext User specific query context - relevant only for derived classes that want
+     * @param queryContext User specific query context - relevant for derived classes that want
      *                 to override some of query processing methods
      * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if {@link #isAccumulateMultiValues()}
      * is {@code true} and multiple values are encountered for an attribute then a {@link List} of them is
      * mapped as its value
      * @throws NamingException If failed to executed the LDAP query
+     * @see #queryAttributes(Object, DirContext, Map, String, String)
      */
-    public Map<String, Object> resolveAttributes(String username, String password, Object queryContext) throws NamingException {
-        DirContext context = initializeDirContext(queryContext, ldapEnv, username, password);
+    public Map<String, Object> resolveAttributes(String username, String password, C queryContext) throws NamingException {
+        // create a copy of the original environment so we can change it
+        DirContext context = initializeDirContext(queryContext, new HashMap<String, Object>(ldapEnv), username, password);
         try {
-            Map<?, ?> ldapConfig = context.getEnvironment();
-            String baseDN = resolveBaseDN(queryContext, ldapConfig, username, password);
-            String filter = resolveSearchFilter(queryContext, ldapConfig, username, password);
-            NamingEnumeration<? extends SearchResult> result =
-                    context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
-                                   ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
-                                   searchControls);
-            try {
-                Map<String, Object> attrsMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
-                String referralMode = Objects.toString(ldapConfig.get(Context.REFERRAL), null);
-                for (int index = 0;; index++) {
-                    if (!result.hasMore()) {
-                        break;
-                    }
-
-                    processSearchResult(queryContext, ldapConfig, attrsMap, index, result.next());
+            return queryAttributes(queryContext, context, context.getEnvironment(), username, password);
+        } finally {
+            context.close();
+        }
+    }
 
-                    // if not following referrals stop at the 1st result regardless if there are others
-                    if ("ignore".equals(referralMode)) {
-                        break;
-                    }
+    /**
+     * @param queryContext The user-specific query context
+     * @param context The initialized {@link DirContext}
+     * @param ldapConfig The LDAP environment setup
+     * @param username The username
+     * @param password The password
+     * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if {@link #isAccumulateMultiValues()}
+     * is {@code true} and multiple values are encountered for an attribute then a {@link List} of them is
+     * mapped as its value
+     * @throws NamingException If failed to executed the LDAP query
+     */
+    protected Map<String, Object> queryAttributes(C queryContext, DirContext context, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+        String baseDN = resolveBaseDN(queryContext, ldapConfig, username, password);
+        String filter = resolveSearchFilter(queryContext, ldapConfig, username, password);
+        NamingEnumeration<? extends SearchResult> result =
+                context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
+                               ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
+                               searchControls);
+        try {
+            Map<String, Object> attrsMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
+            String referralMode = Objects.toString(ldapConfig.get(Context.REFERRAL), null);
+            for (int index = 0;; index++) {
+                if (!result.hasMore()) {
+                    break;
                 }
 
-                return attrsMap;
-            } finally {
-                result.close();
+                processSearchResult(queryContext, ldapConfig, attrsMap, index, result.next());
+
+                // if not following referrals stop at the 1st result regardless if there are others
+                if ("ignore".equals(referralMode)) {
+                    break;
+                }
             }
+
+            return attrsMap;
         } finally {
-            context.close();
+            result.close();
         }
     }
 
-    protected DirContext initializeDirContext(Object queryContext, Map<String, ?> ldapConfig, String username, String password) throws NamingException {
-        Map<String, Object> env;
-        synchronized (ldapConfig) { // create a copy so we can change it
-            env = new HashMap<String, Object>(ldapConfig);
-        }
+    protected DirContext initializeDirContext(C queryContext, Map<String, Object> env, String username, String password) throws NamingException {
+        Map<String, ?> ldapConfig = setupDirContextEnvironment(queryContext, env, username, password);
+        return new InitialDirContext(new Hashtable<String, Object>(ldapConfig));
+    }
 
+    /**
+     * Called in order to set up the environment configuration passed to the
+     * {@link InitialDirContext#InitialDirContext(Hashtable)} constructor
+     *
+     * @param queryContext The caller-specific query context
+     * @param env The current environment setup
+     * @param username The username - may be {@code null}/empty
+     * @param password The password  - may be {@code null}/empty
+     * @return An updated environment configuration - can be a <U>new</U> instance
+     * or just the original one with some changes in it
+     * @throws NamingException If failed to set up the environment
+     */
+    protected Map<String, Object> setupDirContextEnvironment(C queryContext, Map<String, Object> env, String username, String password) throws NamingException {
         if (!env.containsKey(Context.PROVIDER_URL)) {
             int port = getPort();
             ValidateUtils.checkTrue(port > 0, "No port configured");
@@ -391,20 +437,20 @@ public class LdapNetworkConnector extends NetworkConnector {
             }
         }
 
-        return new InitialDirContext(new Hashtable<String, Object>(env));
+        return env;
     }
 
-    protected String resolveBaseDN(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+    protected String resolveBaseDN(C queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
         Object[] bindParams = {username, password};
         return ValidateUtils.checkNotNull(baseDNPattern, "No base DN pattern").format(bindParams);
     }
 
-    protected String resolveSearchFilter(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+    protected String resolveSearchFilter(C queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
         Object[] bindParams = {username, password};
         return ValidateUtils.checkNotNull(searchFilterPattern, "No search filter pattern").format(bindParams);
     }
 
-    protected void processSearchResult(Object queryContext, Map<?, ?> ldapConfig, Map<String, Object> attrsMap,
+    protected void processSearchResult(C queryContext, Map<?, ?> ldapConfig, Map<String, Object> attrsMap,
             int resultIndex, SearchResult result)
                     throws NamingException {
         String dn = result.getName();
@@ -422,7 +468,7 @@ public class LdapNetworkConnector extends NetworkConnector {
     }
 
     // returns the most up-to-date value mapped for the attribute
-    protected Object processResultAttributeValue(Object queryContext, Map<?, ?> ldapConfig,
+    protected Object processResultAttributeValue(C queryContext, Map<?, ?> ldapConfig,
             String dn, int resultIndex, Map<String, Object> attrsMap, Attribute a)
                     throws NamingException {
         String attrID = a.getID();
@@ -460,7 +506,7 @@ public class LdapNetworkConnector extends NetworkConnector {
     }
 
     @SuppressWarnings("unchecked")
-    protected Object accumulateAttributeValue(Object queryContext, Map<String, Object> attrsMap, String attrID, Object attrVal) {
+    protected Object accumulateAttributeValue(C queryContext, Map<String, Object> attrsMap, String attrID, Object attrVal) {
         Object prev = attrsMap.put(attrID, attrVal);
         if (prev == null) {
             return null;    // debug breakpoint

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
new file mode 100644
index 0000000..2544f35
--- /dev/null
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.sshd.server.auth;
+
+import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Serves as the base class for password and public key authenticators.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LdapAuthenticator extends LdapNetworkConnector<ServerSession> {
+    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+
+    public LdapAuthenticator() {
+        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
index 251a3a8..83165e7 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
@@ -23,23 +23,41 @@ import java.util.Map;
 
 import javax.naming.NamingException;
 
-import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.auth.LdapAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
+ * Uses LDAP to authenticate a user and password. By default it can achieve this using 2 ways:
+ * <OL>
+ *      <P><LI>
+ *      Comparing the provided password with the one stored in LDAP. In this case,
+ *      the bind DN and password patterns can be either empty (if anonymous access
+ *      allowed) or can contain the administrative username / password required to
+ *      run the LDAP query. The search filter pattern should be set to require a
+ *      match for <U>both</U> the username and password - e.g., <code>&quot;(&(user={0})(password={1}))&quot;</code>.
+ *      The set default ({@link #DEFAULT_SEARCH_FILTER_PATTERN}) uses the most
+ *      commonly encountered attributes names for this purpose.
+ *      </LI></P>
+ *
+ *      <P><LI>
+ *      Using the original username + password to access LDAP - in which case the very
+ *      success of retrieving anything can be considered a successful authentication.
+ *      In this case, the bind DN and password patterns should be set up to generate
+ *      the correct credentials - the default is to &quot;echo&quot; the provided
+ *      username and password as-is. E.g., if the username is always the alias part
+ *      of a known e-mail, the bind DN should be set to <code>&quot;{0}@my.domain.com&quot;</code>.
+ *      </LI></P>
+ * </OL>
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapPasswordAuthenticator extends LdapNetworkConnector implements PasswordAuthenticator {
-    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+public class LdapPasswordAuthenticator extends LdapAuthenticator implements PasswordAuthenticator {
     public static final String DEFAULT_PASSWORD_ATTR_NAME = "userPassword";
 
     public static final String DEFAULT_SEARCH_FILTER_PATTERN =
             "(&(" + DEFAULT_USERNAME_ATTR_NAME + "={0})(" + DEFAULT_PASSWORD_ATTR_NAME + "={1}))";
-    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
 
     public LdapPasswordAuthenticator() {
         setRetrievedAttributes(null);
-        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
         setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
index fa3e929..ff8f5ce 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
@@ -22,6 +22,10 @@ package org.apache.sshd.server.auth.pubkey;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -29,30 +33,37 @@ import javax.naming.NamingException;
 
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.auth.LdapAuthenticator;
 import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
+ * Uses LDAP to retrieve a user's registered public key and compare it with
+ * the provided one. The default search pattern attempts to retrieve the user's
+ * SSH public key value which is assumed to be in {@code OpenSSH} format. The
+ * default assumes that the value resides in the {@link #DEFAULT_PUBKEY_ATTR_NAME}
+ * attribute and can be either a single or a multi-valued one
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements PublickeyAuthenticator {
-    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
-    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+public class LdapPublickeyAuthenticator extends LdapAuthenticator implements PublickeyAuthenticator {
     public static final String DEFAULT_SEARCH_FILTER_PATTERN = DEFAULT_USERNAME_ATTR_NAME + "={0}";
+    // this seems to be the most commonly used attribute name
     public static final String DEFAULT_PUBKEY_ATTR_NAME = "sshPublicKey";
 
     private String keyAttributeName = DEFAULT_PUBKEY_ATTR_NAME;
 
     public LdapPublickeyAuthenticator() {
-        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
         setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
         setRetrievedAttributes(DEFAULT_PUBKEY_ATTR_NAME);
+        setAccumulateMultiValues(true); // in case multiple keys registered
     }
 
     /**
-     * @return The LDAP attribute name containing the public key in {@code OpenSSH} format
+     * @return The LDAP attribute name containing the public key - assumed
+     * by default to be in {@code OpenSSH} format
      */
     public String getKeyAttributeName() {
         return keyAttributeName;
@@ -79,17 +90,120 @@ public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements
         }
     }
 
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @return {@code true} whether to accept the presented public key
+     * @throws GeneralSecurityException If failed to recover the public key(s)
+     * @throws IOException If failed to parse the public key(s) data
+     * @see #recoverPublicKeys(String, PublicKey, ServerSession, Map, Object)
+     * @see #authenticate(String, PublicKey, ServerSession, Map, Collection)
+     */
     protected boolean authenticate(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs)
             throws GeneralSecurityException, IOException {
         String attrName = getKeyAttributeName();
-        Object keyData = ValidateUtils.checkNotNull(attrs.get(attrName), "No data for attribute=%s", attrName);
-        PublicKey actual = recoverPublicKey(username, expected, session, keyData);
-        return KeyUtils.compareKeys(expected, actual);
+        Collection<PublicKey> keys = recoverPublicKeys(username, expected, session, attrs, attrs.get(attrName));
+        return authenticate(username, expected, session, attrs, keys);
+    }
+
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keys The {@link Collection} of recovered {@link PublicKey}s - may be {@code null}/empty
+     * @return {@code true} whether to accept the presented public key
+     */
+    protected boolean authenticate(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Collection<? extends PublicKey> keys) {
+        if (GenericUtils.isEmpty(keys)) {
+            if (log.isDebugEnabled()) {
+                log.debug("authenticate({}@{}) no registered keys", username, session);
+            }
+            return false;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("authenticate({}@{}) check {} registered keys", username, session, keys.size());
+        }
+
+        for (PublicKey actual : keys) {
+            if (log.isTraceEnabled()) {
+                log.trace("authenticate({}@{}) expected={}-{}, actual={}-{}",
+                          username, session,
+                          KeyUtils.getKeyType(expected), KeyUtils.getFingerPrint(expected),
+                          KeyUtils.getKeyType(actual), KeyUtils.getFingerPrint(actual));
+            }
+
+            if (KeyUtils.compareKeys(expected, actual)) {
+                return true;
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("authenticate({}@{}) no matching keys", username, session);
+        }
+
+        return false;
+    }
+
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keyData The value of the {@link #getKeyAttributeName()} attribute - may be {@code null},
+     * a single object or a collection of such (if multi-valued attribute)
+     * @return A {@link List} of the recovered {@link PublicKey}s - may be {@code null}/empty
+     * @throws GeneralSecurityException If failed to recover the public key(s)
+     * @throws IOException If failed to parse the public key(s) data
+     * @see #parsePublicKeyValue(String, PublicKey, ServerSession, Map, Object)
+     */
+    protected List<PublicKey> recoverPublicKeys(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Object keyData)
+            throws GeneralSecurityException, IOException {
+        // handle case of multi-valued attribute
+        if (keyData instanceof Collection<?>) {
+            Collection<?> values = (Collection<?>) keyData;
+            List<PublicKey> keys = new ArrayList<PublicKey>(values.size());
+            for (Object v : values) {
+                PublicKey k = parsePublicKeyValue(username, expected, session, attrs, v);
+                if (k == null) {
+                    continue;   // debug breakpoint
+                }
+
+                keys.add(k);
+            }
+
+            return keys;
+        }
+
+        PublicKey k = parsePublicKeyValue(username, expected, session, attrs, keyData);
+        return (k == null) ? Collections.<PublicKey>emptyList() : Collections.singletonList(k);
     }
 
-    protected PublicKey recoverPublicKey(String username, PublicKey expected, ServerSession session, Object keyData)
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keyData One of the values (if multi-valued attribute) - may be {@code null}
+     * @return The extracted {@link PublicKey} or {@code null} if none available
+     * @throws GeneralSecurityException If failed to recover the public key
+     * @throws IOException If failed to parse the public key data
+     */
+    protected PublicKey parsePublicKeyValue(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Object keyData)
             throws GeneralSecurityException, IOException {
+        if (keyData == null) {
+            return null;
+        }
+
         AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(Objects.toString(keyData, null));
-        return ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+        PublicKey key = ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+        if (log.isTraceEnabled()) {
+            log.trace("parsePublicKeyValue({}@{}) {}-{}",
+                      username, session, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+        }
+        return key;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
index 2b60fb6..eb115ea 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
@@ -67,6 +67,18 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
         super();
     }
 
+    public static String getHost(Pair<LdapServer, DirectoryService> context) {
+        return getHost((context == null) ? null : context.getFirst());
+    }
+
+    public static String getHost(LdapServer ldapServer) {
+        return getHost((ldapServer == null) ? null : ldapServer.getTransports());
+    }
+
+    public static String getHost(Transport ... transports) {
+        return GenericUtils.isEmpty(transports) ? null : transports[0].getAddress();
+    }
+
     public static int getPort(Pair<LdapServer, DirectoryService> context) {
         return getPort((context == null) ? null : context.getFirst());
     }
@@ -127,15 +139,6 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
             directoryService.setSystemPartition(systemPartition);
         }
 
-        // Create a new partition for the special extra attributes
-        {
-            JdbmPartition partition = new JdbmPartition();
-            partition.setId("openssh-lpk");
-            partition.setSuffix("cn=openssh-lpk,cn=schema,cn=config");
-            partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, partition.getId()))));
-            directoryService.addPartition(partition);
-        }
-
         // Create a new partition for the users
         {
             JdbmPartition partition = new JdbmPartition();
@@ -149,7 +152,7 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
         directoryService.getChangeLog().setEnabled(false);
 
         LdapServer ldapServer = new LdapServer();
-        ldapServer.setTransports(new TcpTransport(PORT));
+        ldapServer.setTransports(new TcpTransport(TEST_LOCALHOST, PORT));
         ldapServer.setDirectoryService(directoryService);
 
         log.info("Starting directory service ...");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
index aa862ca..b7c2f2b 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
@@ -61,11 +61,14 @@ public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest {
 
     @Test   // the user's password is compared with the LDAP stored one
     public void testPasswordComparison() throws Exception {
+        Pair<LdapServer, DirectoryService> ldapContext = ldapContextHolder.get();
         LdapPasswordAuthenticator auth = new LdapPasswordAuthenticator();
+        auth.setHost(getHost(ldapContext));
+        auth.setPort(getPort(ldapContext));
         auth.setBaseDN(BASE_DN_TEST);
-        auth.setPort(getPort(ldapContextHolder.get()));
 
         ServerSession session = Mockito.mock(ServerSession.class);
+        outputDebugMessage("%s: %s", getCurrentTestName(), auth);
         for (Map.Entry<String, String> ue : usersMap.entrySet()) {
             String username = ue.getKey();
             String password = ue.getValue();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
index c697a56..7bf273f 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
@@ -77,13 +77,16 @@ public class LdapPublickeyAuthenticatorTest extends BaseAuthenticatorTest {
 
     @Test
     public void testPublicKeyComparison() throws Exception {
+        Pair<LdapServer, DirectoryService> ldapContext = ldapContextHolder.get();
         LdapPublickeyAuthenticator auth = new LdapPublickeyAuthenticator();
+        auth.setHost(getHost(ldapContext));
+        auth.setPort(getPort(ldapContext));
         auth.setBaseDN(BASE_DN_TEST);
-        auth.setPort(getPort(ldapContextHolder.get()));
         auth.setKeyAttributeName(TEST_ATTR_NAME);
         auth.setRetrievedAttributes(TEST_ATTR_NAME);
 
         ServerSession session = Mockito.mock(ServerSession.class);
+        outputDebugMessage("%s: %s", getCurrentTestName(), auth);
         for (Map.Entry<String, PublicKey> ke : keysMap.entrySet()) {
             String username = ke.getKey();
             PublicKey key = ke.getValue();