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 2021/01/02 07:14:39 UTC

[mina-sshd] branch master updated (2fd8702 -> f4aa59f)

This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git.


    from 2fd8702  [SSHD-1111] Fixed SshClientCliSupport compression option detection
     new b27c0a1  Ugraded ByteBuddy version to 1.10.19
     new 8c0b726  Ugraded Bouncycastle version to 1.68
     new 8992656  Ugraded JGit version to 5.10.0.202012080955-r
     new 0d0af63  Ugraded Checkstyle version to 8.39
     new 62b1264  Ugraded PMD version to 6.30.0
     new 410bfbb  Ugraded PMD plugin version to 3.14.0
     new cb255ca  Ugraded testcontainers version to 1.15.1
     new 71f3145  Ugraded Netty version to 4.1.56.Final
     new 579695b  [SSHD-1114] Added callbacks for client-side password authentication progress
     new a94f456  [SSHD-1114] Added callbacks for client-side public key authentication progress
     new a8ee3aa  [SSHD-1114] Added callbacks for client-side host-based authentication progress
     new 6f0513c  [SSHD-1114] Split AuthenticationTest class into several classes
     new 25a3823  [SSHD-1114] Added capability for interactive password authentication participation via UserInteraction
     new 1067784  [SSHD-1114] Added capability for interactive key based authentication participation via UserInteraction
     new f4aa59f  [SSHD-1114] Added HostBasedAuthenticationReporter#signalAuthenticationExhausted

The 15 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGES.md                                         |   5 +
 docs/client-setup.md                               |  38 +-
 docs/event-listeners.md                            |  18 +
 pom.xml                                            |  12 +-
 .../sshd/client/auth/pubkey/PublicKeyIdentity.java |   6 +-
 .../apache/sshd/util/test/JUnitTestSupport.java    |  13 +
 .../main/java/org/apache/sshd/agent/SshAgent.java  |  10 +
 .../sshd/agent/common/AbstractAgentProxy.java      |   1 +
 .../org/apache/sshd/agent/local/AgentImpl.java     |   6 +
 .../sshd/client/ClientAuthenticationManager.java   |  15 +
 .../java/org/apache/sshd/client/SshClient.java     |  38 +-
 .../java/org/apache/sshd/client/auth/UserAuth.java |  34 +-
 .../hostbased/HostBasedAuthenticationReporter.java |  99 +++
 .../client/auth/hostbased/UserAuthHostBased.java   |  58 +-
 .../sshd/client/auth/keyboard/UserInteraction.java |  24 +-
 .../password/PasswordAuthenticationReporter.java   |  84 ++
 .../client/auth/password/UserAuthPassword.java     |  53 +-
 .../sshd/client/auth/pubkey/KeyAgentIdentity.java  |  20 +-
 .../sshd/client/auth/pubkey/KeyPairIdentity.java   |  12 +-
 .../pubkey/PublicKeyAuthenticationReporter.java    | 106 +++
 .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 104 ++-
 .../sshd/client/session/AbstractClientSession.java |  42 +
 .../sshd/client/session/ClientUserAuthService.java |  15 +-
 .../client/ClientAuthenticationManagerTest.java    |  36 +-
 .../sshd/common/auth/AuthenticationTest.java       | 863 +--------------------
 .../common/auth/AuthenticationTestSupport.java     | 105 +++
 .../common/auth/HostBasedAuthenticationTest.java   | 150 ++++
 .../KeyboardInteractiveAuthenticationTest.java     | 233 ++++++
 .../common/auth/PasswordAuthenticationTest.java    | 502 ++++++++++++
 .../common/auth/PublicKeyAuthenticationTest.java   | 392 ++++++++++
 .../sshd/util/test/server/TestServerSession.java}  |  23 +-
 sshd-netty/pom.xml                                 |   2 +-
 sshd-sftp/pom.xml                                  |   2 +-
 33 files changed, 2174 insertions(+), 947 deletions(-)
 create mode 100644 sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
 create mode 100644 sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
 create mode 100644 sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
 create mode 100644 sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java
 create mode 100644 sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java
 create mode 100644 sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java
 create mode 100644 sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
 create mode 100644 sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
 copy sshd-core/src/{main/java/org/apache/sshd/server/auth/UserAuthNone.java => test/java/org/apache/sshd/util/test/server/TestServerSession.java} (65%)


[mina-sshd] 03/15: Ugraded JGit version to 5.10.0.202012080955-r

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 89926569556c5a054770a49b3a42e389d8cd5b30
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:52:40 2020 +0200

    Ugraded JGit version to 5.10.0.202012080955-r
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 23853b5..8b3dfa8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,7 +108,7 @@
         <slf4j.version>1.7.30</slf4j.version>
         <logback.version>1.2.3</logback.version>        
         <spring.version>5.3.2</spring.version>
-        <jgit.version>5.9.0.202009080501-r</jgit.version>
+        <jgit.version>5.10.0.202012080955-r</jgit.version>
         <junit.version>4.13.1</junit.version>
         <bytebuddy.version>1.10.19</bytebuddy.version>
 


[mina-sshd] 09/15: [SSHD-1114] Added callbacks for client-side password authentication progress

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 579695bb757c067b1f59335219dc7201c7f2006e
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 22:10:45 2020 +0200

    [SSHD-1114] Added callbacks for client-side password authentication progress
---
 CHANGES.md                                         |  1 +
 docs/event-listeners.md                            |  6 ++
 .../sshd/client/ClientAuthenticationManager.java   |  5 ++
 .../java/org/apache/sshd/client/SshClient.java     | 14 ++++-
 .../java/org/apache/sshd/client/auth/UserAuth.java | 34 ++++++++++-
 .../password/PasswordAuthenticationReporter.java   | 70 ++++++++++++++++++++++
 .../client/auth/password/UserAuthPassword.java     | 29 ++++++++-
 .../sshd/client/session/AbstractClientSession.java | 14 +++++
 .../sshd/client/session/ClientUserAuthService.java | 15 ++++-
 .../client/ClientAuthenticationManagerTest.java    | 14 ++++-
 .../sshd/common/auth/AuthenticationTest.java       | 52 +++++++++++++++-
 11 files changed, 244 insertions(+), 10 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index da9bb6c..75ae5f7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -26,3 +26,4 @@
 
 * [SSHD-1085](https://issues.apache.org/jira/browse/SSHD-1085) Added more notifications related to channel state change for detecting channel closing or closed earlier.
 * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests
+* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index fb31d55..3f9dcf6 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -193,3 +193,9 @@ the handler decided not to intervene.
 Informs about signal requests as described in [RFC 4254 - section 6.9](https://tools.ietf.org/html/rfc4254#section-6.9), "break" requests
 (sent as SIGINT) as described in [RFC 4335](https://tools.ietf.org/html/rfc4335) and "window-change" (sent as SIGWINCH) requests as described
 in [RFC 4254 - section 6.7](https://tools.ietf.org/html/rfc4254#section-6.7)
+
+### `PasswordAuthenticationReporter`
+
+Used to inform about the progress of the client-side password based authentication as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
+Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus
+overriding any globally registered instance.
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
index b42eae5..14bca04 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
@@ -28,6 +28,7 @@ import org.apache.sshd.client.auth.BuiltinUserAuthFactories;
 import org.apache.sshd.client.auth.UserAuth;
 import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
@@ -106,6 +107,10 @@ public interface ClientAuthenticationManager
 
     void setUserInteraction(UserInteraction userInteraction);
 
+    PasswordAuthenticationReporter getPasswordAuthenticationReporter();
+
+    void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter);
+
     @Override
     default void setUserAuthFactoriesNames(Collection<String> names) {
         BuiltinUserAuthFactories.ParseResult result = BuiltinUserAuthFactories.parseFactoriesList(names);
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index d5873eb..4182bf8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -46,6 +46,7 @@ import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
@@ -170,7 +171,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
 
     protected IoConnector connector;
     protected SessionFactory sessionFactory;
-    protected UserInteraction userInteraction;
     protected List<UserAuthFactory> userAuthFactories;
 
     private ClientProxyConnector proxyConnector;
@@ -180,6 +180,8 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private KeyIdentityProvider keyIdentityProvider;
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
+    private UserInteraction userInteraction;
+    private PasswordAuthenticationReporter passwordAuthenticationReporter;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -258,6 +260,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
+    public PasswordAuthenticationReporter getPasswordAuthenticationReporter() {
+        return passwordAuthenticationReporter;
+    }
+
+    @Override
+    public void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter) {
+        this.passwordAuthenticationReporter = reporter;
+    }
+
+    @Override
     public List<UserAuthFactory> getUserAuthFactories() {
         return userAuthFactories;
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuth.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuth.java
index 4ac1bf9..1822584 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuth.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/UserAuth.java
@@ -18,6 +18,8 @@
  */
 package org.apache.sshd.client.auth;
 
+import java.util.List;
+
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionHolder;
 import org.apache.sshd.common.auth.UserAuthInstance;
@@ -25,7 +27,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
  * Represents a user authentication mechanism
- * 
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public interface UserAuth extends ClientSessionHolder, UserAuthInstance<ClientSession> {
@@ -46,6 +48,36 @@ public interface UserAuth extends ClientSessionHolder, UserAuthInstance<ClientSe
     boolean process(Buffer buffer) throws Exception;
 
     /**
+     * Signal reception of {@code SSH_MSG_USERAUTH_SUCCESS} message
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  buffer    The {@link Buffer} containing the success message (after having consumed the relevant data from
+     *                   it)
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception {
+        // ignored
+    }
+
+    /**
+     * Signals reception of {@code SSH_MSG_USERAUTH_FAILURE} message
+     *
+     * @param  session       The {@link ClientSession}
+     * @param  service       The requesting service name
+     * @param  partial       {@code true} if some partial authentication success so far
+     * @param  serverMethods The {@link List} of authentication methods that can continue
+     * @param  buffer        The {@link Buffer} containing the failure message (after having consumed the relevant data
+     *                       from it)
+     * @throws Exception     If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthMethodFailure(
+            ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer)
+            throws Exception {
+        // ignored
+    }
+
+    /**
      * Called to release any allocated resources
      */
     void destroy();
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
new file mode 100644
index 0000000..3ebdb71
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
@@ -0,0 +1,70 @@
+/*
+ * 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.client.auth.password;
+
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * Used to inform the about the progress of a password authentication
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see    <a href="https://tools.ietf.org/html/rfc4252#section-8">RFC-4252 section 8</a>
+ */
+public interface PasswordAuthenticationReporter {
+    /**
+     * @param  session     The {@link ClientSession}
+     * @param  service     The requesting service name
+     * @param  oldPassword The password being attempted
+     * @param  modified    {@code true} if this is an attempt due to {@code SSH_MSG_USERAUTH_PASSWD_CHANGEREQ}
+     * @param  newPassword The changed password
+     * @throws Exception   If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationAttempt(
+            ClientSession session, String service, String oldPassword, boolean modified, String newPassword)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  password  The password that was attempted
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationSuccess(ClientSession session, String service, String password) throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session       The {@link ClientSession}
+     * @param  service       The requesting service name
+     * @param  password      The password that was attempted
+     * @param  partial       {@code true} if some partial authentication success so far
+     * @param  serverMethods The {@link List} of authentication methods that can continue
+     * @throws Exception     If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationFailure(
+            ClientSession session, String service, String password, boolean partial, List<String> serverMethods)
+            throws Exception {
+        // ignored
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
index 6fcad9b..e99dfe8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
@@ -18,8 +18,8 @@
  */
 package org.apache.sshd.client.auth.password;
 
-import java.io.IOException;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 
 import org.apache.sshd.client.auth.AbstractUserAuth;
@@ -157,11 +157,11 @@ public class UserAuthPassword extends AbstractUserAuth {
      * @param  newPassword The new password
      * @return             An {@link IoWriteFuture} that can be used to wait and check on the success/failure of the
      *                     request packet being sent
-     * @throws IOException If failed to send the message.
+     * @throws Exception   If failed to send the message.
      */
     protected IoWriteFuture sendPassword(
             Buffer buffer, ClientSession session, String oldPassword, String newPassword)
-            throws IOException {
+            throws Exception {
         String username = session.getUsername();
         String service = getService();
         String name = getName();
@@ -187,6 +187,29 @@ public class UserAuthPassword extends AbstractUserAuth {
             buffer.putString(newPassword);
         }
 
+        PasswordAuthenticationReporter reporter = session.getPasswordAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationAttempt(session, service, oldPassword, modified, newPassword);
+        }
+
         return session.writePacket(buffer);
     }
+
+    @Override
+    public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception {
+        PasswordAuthenticationReporter reporter = session.getPasswordAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationSuccess(session, service, current);
+        }
+    }
+
+    @Override
+    public void signalAuthMethodFailure(
+            ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer)
+            throws Exception {
+        PasswordAuthenticationReporter reporter = session.getPasswordAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationFailure(session, service, current, partial, serverMethods);
+        }
+    }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index c7c8d5e..4c5eff4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -33,6 +33,7 @@ import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
@@ -90,6 +91,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private ServerKeyVerifier serverKeyVerifier;
     private UserInteraction userInteraction;
     private PasswordIdentityProvider passwordIdentityProvider;
+    private PasswordAuthenticationReporter passwordAuthenticationReporter;
     private KeyIdentityProvider keyIdentityProvider;
     private List<UserAuthFactory> userAuthFactories;
     private SocketAddress connectAddress;
@@ -161,6 +163,18 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public PasswordAuthenticationReporter getPasswordAuthenticationReporter() {
+        ClientFactoryManager manager = getFactoryManager();
+        return resolveEffectiveProvider(PasswordAuthenticationReporter.class, passwordAuthenticationReporter,
+                manager.getPasswordAuthenticationReporter());
+    }
+
+    @Override
+    public void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter) {
+        this.passwordAuthenticationReporter = reporter;
+    }
+
+    @Override
     public List<UserAuthFactory> getUserAuthFactories() {
         ClientFactoryManager manager = getFactoryManager();
         return resolveEffectiveFactories(userAuthFactories, manager.getUserAuthFactories());
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
index ab2b55c..fb430b1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -239,9 +240,14 @@ public class ClientUserAuthService extends AbstractCloseable implements Service,
                 log.debug("processUserAuth({}) SSH_MSG_USERAUTH_SUCCESS Succeeded with {}",
                         session, (userAuth == null) ? "<unknown>" : userAuth.getName());
             }
+
             if (userAuth != null) {
                 try {
-                    userAuth.destroy();
+                    try {
+                        userAuth.signalAuthMethodSuccess(session, service, buffer);
+                    } finally {
+                        userAuth.destroy();
+                    }
                 } finally {
                     userAuth = null;
                 }
@@ -267,7 +273,12 @@ public class ClientUserAuthService extends AbstractCloseable implements Service,
                 currentMethod = 0;
                 if (userAuth != null) {
                     try {
-                        userAuth.destroy();
+                        try {
+                            userAuth.signalAuthMethodFailure(
+                                    session, service, partial, Collections.unmodifiableList(serverMethods), buffer);
+                        } finally {
+                            userAuth.destroy();
+                        }
                     } finally {
                         userAuth = null;
                     }
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
index 86adf44..40df790 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
@@ -31,6 +31,7 @@ import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.BuiltinUserAuthFactories;
 import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
@@ -100,6 +101,16 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport {
             }
 
             @Override
+            public PasswordAuthenticationReporter getPasswordAuthenticationReporter() {
+                return null;
+            }
+
+            @Override
+            public void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter) {
+                throw new UnsupportedOperationException("setPasswordAuthenticationReporter(" + reporter + ")");
+            }
+
+            @Override
             public ServerKeyVerifier getServerKeyVerifier() {
                 return null;
             }
@@ -183,7 +194,8 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport {
                         PasswordIdentityProvider.class,
                         ServerKeyVerifier.class,
                         UserInteraction.class,
-                        KeyIdentityProvider.class
+                        KeyIdentityProvider.class,
+                        PasswordAuthenticationReporter.class
                 }) {
                     testClientProvidersPropagation(provider, client, session);
                 }
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
index 0e748d8..2dbe763 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
@@ -25,6 +25,7 @@ import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.PublicKey;
 import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -39,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.session.ClientSession;
@@ -236,7 +238,7 @@ public class AuthenticationTest extends BaseTestSupport {
                                 @Override
                                 protected IoWriteFuture sendPassword(
                                         Buffer buffer, ClientSession session, String oldPassword, String newPassword)
-                                        throws IOException {
+                                        throws Exception {
                                     int count = sentCount.incrementAndGet();
                                     // 1st one is the original one (which is denied by the server)
                                     // 2nd one is the updated one retrieved from the user interaction
@@ -970,7 +972,7 @@ public class AuthenticationTest extends BaseTestSupport {
                 session.auth().verify(AUTH_TIMEOUT);
 
                 KeyExchange kex = session.getKex();
-                assertNull("KEX no nullified after completion", kex);
+                assertNull("KEX not nullified after completion", kex);
 
                 actualKey = session.getServerKey();
             } finally {
@@ -990,6 +992,52 @@ public class AuthenticationTest extends BaseTestSupport {
         fail("No matching server key found for " + actualKey);
     }
 
+    @Test   // see SSHD-1114
+    public void testPasswordAuthenticationReporter() throws Exception {
+        String goodPassword = getCurrentTestName();
+        String badPassword = getClass().getSimpleName();
+        List<String> actual = new ArrayList<>();
+        PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, String oldPassword, boolean modified, String newPassword)
+                    throws Exception {
+                actual.add(oldPassword);
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(ClientSession session, String service, String password)
+                    throws Exception {
+                assertEquals("Mismatched succesful password", goodPassword, password);
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, String password, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                assertEquals("Mismatched failed password", badPassword, password);
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.addPasswordIdentity(badPassword);
+                session.addPasswordIdentity(goodPassword);
+                session.setPasswordAuthenticationReporter(reporter);
+                session.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+
+        assertListEquals("Attempted passwords", Arrays.asList(badPassword, goodPassword), actual);
+    }
+
     private static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException {
         assertTrue(message + ": failed to get result on time", future.await(AUTH_TIMEOUT));
         assertEquals(message + ": mismatched authentication result", expected, future.isSuccess());


[mina-sshd] 07/15: Ugraded testcontainers version to 1.15.1

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit cb255ca51044da8af7bd35f8be77ab785d5194cd
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:56:41 2020 +0200

    Ugraded testcontainers version to 1.15.1
---
 sshd-sftp/pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshd-sftp/pom.xml b/sshd-sftp/pom.xml
index a331962..d45c797 100644
--- a/sshd-sftp/pom.xml
+++ b/sshd-sftp/pom.xml
@@ -90,7 +90,7 @@
                 <groupId>org.testcontainers</groupId>
                 <artifactId>testcontainers-bom</artifactId>
                 <type>pom</type>
-                <version>1.15.0</version>
+                <version>1.15.1</version>
                 <scope>import</scope>
             </dependency>
         </dependencies>


[mina-sshd] 04/15: Ugraded Checkstyle version to 8.39

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 0d0af63ba20385be68bb6f43fba53bf6390c48c3
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:53:13 2020 +0200

    Ugraded Checkstyle version to 8.39
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 8b3dfa8..0885742 100644
--- a/pom.xml
+++ b/pom.xml
@@ -902,7 +902,7 @@
                         <dependency>
                             <groupId>com.puppycrawl.tools</groupId>
                             <artifactId>checkstyle</artifactId>
-                            <version>8.38</version>
+                            <version>8.39</version>
                             <exclusions>
                                 <!-- MCHECKSTYLE-156 -->
                                 <exclusion>


[mina-sshd] 12/15: [SSHD-1114] Split AuthenticationTest class into several classes

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 6f0513c18876476cc5602f1489597e27d7552fa6
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 1 06:58:35 2021 +0200

    [SSHD-1114] Split AuthenticationTest class into several classes
---
 .../sshd/common/auth/AuthenticationTest.java       | 1046 +-------------------
 .../common/auth/AuthenticationTestSupport.java     |  105 ++
 .../common/auth/HostBasedAuthenticationTest.java   |  150 +++
 .../KeyboardInteractiveAuthenticationTest.java     |  233 +++++
 .../common/auth/PasswordAuthenticationTest.java    |  440 ++++++++
 .../common/auth/PublicKeyAuthenticationTest.java   |  327 ++++++
 .../sshd/util/test/server/TestServerSession.java   |   39 +
 7 files changed, 1295 insertions(+), 1045 deletions(-)

diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
index beb3141..d3aece6 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
@@ -19,556 +19,33 @@
 package org.apache.sshd.common.auth;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.PublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
 
 import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
-import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
-import org.apache.sshd.client.auth.keyboard.UserInteraction;
-import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
-import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
-import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.AttributeRepository;
-import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.kex.KeyExchange;
-import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.session.SessionListener;
-import org.apache.sshd.common.signature.BuiltinSignatures;
-import org.apache.sshd.common.signature.Signature;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.io.resource.URLResource;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.apache.sshd.common.util.security.SecurityUtils;
-import org.apache.sshd.core.CoreModuleProperties;
-import org.apache.sshd.server.ServerFactoryManager;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;
-import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
-import org.apache.sshd.server.auth.keyboard.InteractiveChallenge;
 import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
-import org.apache.sshd.server.auth.keyboard.PromptEntry;
-import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
 import org.apache.sshd.server.auth.password.PasswordAuthenticator;
-import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
-import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
-import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
-import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
-import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.server.session.ServerSessionImpl;
-import org.apache.sshd.server.session.SessionFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.CommonTestSupportUtils;
-import org.apache.sshd.util.test.CoreTestSupportUtils;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.FixMethodOrder;
 import org.junit.Test;
 import org.junit.runners.MethodSorters;
 
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AuthenticationTest extends BaseTestSupport {
-
-    private static final AttributeRepository.AttributeKey<Boolean> PASSWORD_ATTR = new AttributeRepository.AttributeKey<>();
-
-    private SshServer sshd;
-    private int port;
-
+public class AuthenticationTest extends AuthenticationTestSupport {
     public AuthenticationTest() {
         super();
     }
 
-    @Before
-    public void setUp() throws Exception {
-        sshd = setupTestServer();
-        sshd.setSessionFactory(new SessionFactory(sshd) {
-            @Override
-            protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception {
-                return new TestSession(getServer(), ioSession);
-            }
-        });
-        sshd.start();
-        port = sshd.getPort();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (sshd != null) {
-            sshd.stop(true);
-        }
-    }
-
-    @Test
-    public void testWrongPassword() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            client.start();
-            try (ClientSession s = client.connect("user", TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.addPasswordIdentity("bad password");
-                assertAuthenticationResult(getCurrentTestName(), s.auth(), false);
-            }
-        }
-    }
-
-    @Test
-    public void testChangeUser() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            client.start();
-
-            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                Collection<ClientSession.ClientSessionEvent> mask
-                        = EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH);
-                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(mask, DEFAULT_TIMEOUT);
-                assertFalse("Timeout while waiting on session events",
-                        result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
-
-                String password = "the-password";
-                for (String username : new String[] { "user1", "user2" }) {
-                    try {
-                        assertAuthenticationResult(username, authPassword(s, username, password), false);
-                    } finally {
-                        s.removePasswordIdentity(password);
-                    }
-                }
-
-                // Note that WAIT_AUTH flag should be false, but since the internal
-                // authentication future is not updated, it's still returned
-                result = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), DEFAULT_TIMEOUT);
-                assertTrue("Mismatched client session close mask: " + result, result.containsAll(mask));
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-196
-    public void testChangePassword() throws Exception {
-        PasswordAuthenticator delegate = sshd.getPasswordAuthenticator();
-        AtomicInteger attemptsCount = new AtomicInteger(0);
-        AtomicInteger changesCount = new AtomicInteger(0);
-        sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
-            @Override
-            public boolean authenticate(String username, String password, ServerSession session) {
-                if (attemptsCount.incrementAndGet() == 1) {
-                    throw new PasswordChangeRequiredException(
-                            attemptsCount.toString(),
-                            getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault());
-                }
-
-                return delegate.authenticate(username, password, session);
-            }
-
-            @Override
-            public boolean handleClientPasswordChangeRequest(
-                    ServerSession session, String username, String oldPassword, String newPassword) {
-                if (changesCount.incrementAndGet() == 1) {
-                    assertNotEquals("Non-different passwords", oldPassword, newPassword);
-                    return authenticate(username, newPassword, session);
-                } else {
-                    return PasswordAuthenticator.super.handleClientPasswordChangeRequest(
-                            session, username, oldPassword, newPassword);
-                }
-            }
-        });
-        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME);
-
-        try (SshClient client = setupTestClient()) {
-            AtomicInteger updatesCount = new AtomicInteger(0);
-            client.setUserInteraction(new UserInteraction() {
-                @Override
-                public boolean isInteractionAllowed(ClientSession session) {
-                    return true;
-                }
-
-                @Override
-                public String[] interactive(
-                        ClientSession session, String name, String instruction,
-                        String lang, String[] prompt, boolean[] echo) {
-                    throw new UnsupportedOperationException("Unexpected call");
-                }
-
-                @Override
-                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
-                    assertEquals("Mismatched prompt", getCurrentTestName(), prompt);
-                    assertEquals("Mismatched language",
-                            CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang);
-                    assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet());
-                    return getCurrentTestName();
-                }
-            });
-
-            AtomicInteger sentCount = new AtomicInteger(0);
-            client.setUserAuthFactories(Collections.singletonList(
-                    new org.apache.sshd.client.auth.password.UserAuthPasswordFactory() {
-                        @Override
-                        public org.apache.sshd.client.auth.password.UserAuthPassword createUserAuth(ClientSession session)
-                                throws IOException {
-                            return new org.apache.sshd.client.auth.password.UserAuthPassword() {
-                                @Override
-                                protected IoWriteFuture sendPassword(
-                                        Buffer buffer, ClientSession session, String oldPassword, String newPassword)
-                                        throws Exception {
-                                    int count = sentCount.incrementAndGet();
-                                    // 1st one is the original one (which is denied by the server)
-                                    // 2nd one is the updated one retrieved from the user interaction
-                                    if (count == 2) {
-                                        return super.sendPassword(buffer, session, getClass().getName(), newPassword);
-                                    } else {
-                                        return super.sendPassword(buffer, session, oldPassword, newPassword);
-                                    }
-                                }
-                            };
-                        }
-                    }));
-            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME);
-
-            client.start();
-
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.addPasswordIdentity(getCurrentTestName());
-                s.auth().verify(AUTH_TIMEOUT);
-                assertEquals("No password change request generated", 2, attemptsCount.get());
-                assertEquals("No password change handled", 1, changesCount.get());
-                assertEquals("No user interaction invoked", 1, updatesCount.get());
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test
-    public void testAuthPasswordOnly() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
-
-            client.start();
-            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
-                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
-                        DEFAULT_TIMEOUT);
-                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
-
-                String password = getCurrentTestName();
-                try {
-                    assertAuthenticationResult(getCurrentTestName(),
-                            authPassword(s, getCurrentTestName(), password), false);
-                } finally {
-                    s.removePasswordIdentity(password);
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test
-    public void testAuthKeyPassword() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
-            sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
-
-            client.start();
-
-            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
-                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
-                        DEFAULT_TIMEOUT);
-                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
-
-                KeyPairProvider provider = createTestHostKeyProvider();
-                KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE);
-                try {
-                    assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY,
-                            authPublicKey(s, getCurrentTestName(), pair), false);
-                } finally {
-                    s.removePublicKeyIdentity(pair);
-                }
-
-                String password = getCurrentTestName();
-                try {
-                    assertAuthenticationResult(UserAuthMethodFactory.PASSWORD,
-                            authPassword(s, getCurrentTestName(), password), true);
-                } finally {
-                    s.removePasswordIdentity(password);
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-612
-    public void testAuthDefaultKeyInteractive() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
-            sshd.setKeyboardInteractiveAuthenticator(new DefaultKeyboardInteractiveAuthenticator() {
-                @Override
-                public InteractiveChallenge generateChallenge(
-                        ServerSession session, String username, String lang, String subMethods)
-                        throws Exception {
-                    assertEquals("Mismatched user language",
-                            CoreModuleProperties.INTERACTIVE_LANGUAGE_TAG.getRequired(client),
-                            lang);
-                    assertEquals("Mismatched client sub-methods",
-                            CoreModuleProperties.INTERACTIVE_SUBMETHODS.getRequired(client),
-                            subMethods);
-
-                    InteractiveChallenge challenge = super.generateChallenge(session, username, lang, subMethods);
-                    assertEquals("Mismatched interaction name", getInteractionName(session), challenge.getInteractionName());
-                    assertEquals("Mismatched interaction instruction", getInteractionInstruction(session),
-                            challenge.getInteractionInstruction());
-                    assertEquals("Mismatched language tag", getInteractionLanguage(session), challenge.getLanguageTag());
-
-                    List<PromptEntry> entries = challenge.getPrompts();
-                    assertEquals("Mismatched prompts count", 1, GenericUtils.size(entries));
-
-                    PromptEntry entry = entries.get(0);
-                    assertEquals("Mismatched prompt", getInteractionPrompt(session), entry.getPrompt());
-                    assertEquals("Mismatched echo", isInteractionPromptEchoEnabled(session), entry.isEcho());
-
-                    return challenge;
-                }
-
-                @Override
-                public boolean authenticate(
-                        ServerSession session, String username, List<String> responses)
-                        throws Exception {
-                    return super.authenticate(session, username, responses);
-                }
-
-            });
-            client.start();
-
-            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
-                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
-                        DEFAULT_TIMEOUT);
-                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
-
-                KeyPairProvider provider = createTestHostKeyProvider();
-                KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE);
-                try {
-                    assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY,
-                            authPublicKey(s, getCurrentTestName(), pair), false);
-                } finally {
-                    s.removePublicKeyIdentity(pair);
-                }
-
-                try {
-                    assertAuthenticationResult(UserAuthMethodFactory.KB_INTERACTIVE,
-                            authInteractive(s, getCurrentTestName(), getCurrentTestName()), true);
-                } finally {
-                    s.setUserInteraction(null);
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-563
-    public void testAuthMultiChallengeKeyInteractive() throws Exception {
-        Class<?> anchor = getClass();
-        InteractiveChallenge challenge = new InteractiveChallenge();
-        challenge.setInteractionName(getCurrentTestName());
-        challenge.setInteractionInstruction(anchor.getPackage().getName());
-        challenge.setLanguageTag(Locale.getDefault().getLanguage());
-
-        Map<String, String> rspMap = NavigableMapBuilder.<String, String> builder(String.CASE_INSENSITIVE_ORDER)
-                .put("class", anchor.getSimpleName())
-                .put("package", anchor.getPackage().getName())
-                .put("test", getCurrentTestName())
-                .build();
-        for (String prompt : rspMap.keySet()) {
-            challenge.addPrompt(prompt, (GenericUtils.size(challenge.getPrompts()) & 0x1) != 0);
-        }
-
-        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME);
-        AtomicInteger genCount = new AtomicInteger(0);
-        AtomicInteger authCount = new AtomicInteger(0);
-        sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() {
-            @Override
-            public InteractiveChallenge generateChallenge(
-                    ServerSession session, String username, String lang, String subMethods)
-                    throws Exception {
-                assertEquals("Unexpected challenge call", 1, genCount.incrementAndGet());
-                return challenge;
-            }
-
-            @Override
-            public boolean authenticate(
-                    ServerSession session, String username, List<String> responses)
-                    throws Exception {
-                assertEquals("Unexpected authenticate call", 1, authCount.incrementAndGet());
-                assertEquals("Mismatched number of responses", GenericUtils.size(rspMap), GenericUtils.size(responses));
-
-                int index = 0;
-                // Cannot use forEach because the index is not effectively final
-                for (Map.Entry<String, String> re : rspMap.entrySet()) {
-                    String prompt = re.getKey();
-                    String expected = re.getValue();
-                    String actual = responses.get(index);
-                    assertEquals("Mismatched response for prompt=" + prompt, expected, actual);
-                    index++;
-                }
-                return true;
-            }
-        });
-        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME);
-
-        try (SshClient client = setupTestClient()) {
-            AtomicInteger interactiveCount = new AtomicInteger(0);
-            client.setUserInteraction(new UserInteraction() {
-                @Override
-                public boolean isInteractionAllowed(ClientSession session) {
-                    return true;
-                }
-
-                @Override
-                public String[] interactive(
-                        ClientSession session, String name, String instruction,
-                        String lang, String[] prompt, boolean[] echo) {
-                    assertEquals("Unexpected multiple calls", 1, interactiveCount.incrementAndGet());
-                    assertEquals("Mismatched name", challenge.getInteractionName(), name);
-                    assertEquals("Mismatched instruction", challenge.getInteractionInstruction(), instruction);
-                    assertEquals("Mismatched language", challenge.getLanguageTag(), lang);
-
-                    List<PromptEntry> entries = challenge.getPrompts();
-                    assertEquals("Mismatched prompts count", GenericUtils.size(entries), GenericUtils.length(prompt));
-
-                    String[] responses = new String[prompt.length];
-                    for (int index = 0; index < prompt.length; index++) {
-                        PromptEntry e = entries.get(index);
-                        String key = e.getPrompt();
-                        assertEquals("Mismatched prompt at index=" + index, key, prompt[index]);
-                        assertEquals("Mismatched echo at index=" + index, e.isEcho(), echo[index]);
-                        responses[index] = ValidateUtils.checkNotNull(rspMap.get(key), "No value for prompt=%s", key);
-                    }
-
-                    return responses;
-                }
-
-                @Override
-                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
-                    throw new UnsupportedOperationException("Unexpected call");
-                }
-            });
-            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthKeyboardInteractiveFactory.NAME);
-
-            client.start();
-
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.auth().verify(AUTH_TIMEOUT);
-                assertEquals("Bad generated challenge count", 1, genCount.get());
-                assertEquals("Bad authentication count", 1, authCount.get());
-                assertEquals("Bad interactive count", 1, interactiveCount.get());
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-196
-    public void testAuthPasswordChangeRequest() throws Exception {
-        PasswordAuthenticator delegate = Objects.requireNonNull(sshd.getPasswordAuthenticator(), "No password authenticator");
-        AtomicInteger attemptsCount = new AtomicInteger(0);
-        sshd.setPasswordAuthenticator((username, password, session) -> {
-            if (attemptsCount.incrementAndGet() == 1) {
-                throw new PasswordChangeRequiredException(
-                        attemptsCount.toString(),
-                        getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault());
-            }
-
-            return delegate.authenticate(username, password, session);
-        });
-        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME);
-
-        try (SshClient client = setupTestClient()) {
-            AtomicInteger updatesCount = new AtomicInteger(0);
-            client.setUserInteraction(new UserInteraction() {
-                @Override
-                public boolean isInteractionAllowed(ClientSession session) {
-                    return true;
-                }
-
-                @Override
-                public String[] interactive(
-                        ClientSession session, String name, String instruction,
-                        String lang, String[] prompt, boolean[] echo) {
-                    throw new UnsupportedOperationException("Unexpected call");
-                }
-
-                @Override
-                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
-                    assertEquals("Mismatched prompt", getCurrentTestName(), prompt);
-                    assertEquals("Mismatched language",
-                            CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang);
-                    assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet());
-                    return getCurrentTestName();
-                }
-            });
-            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME);
-
-            client.start();
-
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.addPasswordIdentity(getCurrentTestName());
-                s.auth().verify(AUTH_TIMEOUT);
-                assertEquals("No password change request generated", 2, attemptsCount.get());
-                assertEquals("No user interaction invoked", 1, updatesCount.get());
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
     @Test // see SSHD-600
     public void testAuthExceptionPropagation() throws Exception {
         try (SshClient client = setupTestClient()) {
@@ -619,257 +96,6 @@ public class AuthenticationTest extends BaseTestSupport {
         }
     }
 
-    @Test
-    public void testPasswordIdentityProviderPropagation() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            List<String> passwords = Collections.singletonList(getCurrentTestName());
-            AtomicInteger loadCount = new AtomicInteger(0);
-            PasswordIdentityProvider provider = () -> {
-                loadCount.incrementAndGet();
-                outputDebugMessage("loadPasswords - count=%s", loadCount);
-                return passwords;
-            };
-            client.setPasswordIdentityProvider(provider);
-
-            client.start();
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.auth().verify(AUTH_TIMEOUT);
-                assertEquals("Mismatched load passwords count", 1, loadCount.get());
-                assertSame("Mismatched passwords identity provider", provider, s.getPasswordIdentityProvider());
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-618
-    public void testPublicKeyAuthDifferentThanKex() throws Exception {
-        KeyPairProvider serverKeys = KeyPairProvider.wrap(
-                CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024),
-                CommonTestSupportUtils.generateKeyPair(KeyUtils.DSS_ALGORITHM, 512),
-                CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256));
-        sshd.setKeyPairProvider(serverKeys);
-        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
-        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
-
-        KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
-        sshd.setPublickeyAuthenticator((username, key, session) -> {
-            String keyType = KeyUtils.getKeyType(key);
-            String expType = KeyUtils.getKeyType(clientIdentity);
-            assertEquals("Mismatched client key types", expType, keyType);
-            assertKeyEquals("Mismatched authentication public keys", clientIdentity.getPublic(), key);
-            return true;
-        });
-
-        // since we need to use RSA
-        CoreTestSupportUtils.setupFullSignaturesSupport(sshd);
-        try (SshClient client = setupTestClient()) {
-            // force server to use only RSA
-            NamedFactory<Signature> kexSignature = BuiltinSignatures.rsa;
-            client.setSignatureFactories(Collections.singletonList(kexSignature));
-            client.setServerKeyVerifier((sshClientSession, remoteAddress, serverKey) -> {
-                String keyType = KeyUtils.getKeyType(serverKey);
-                String expType = kexSignature.getName();
-                assertEquals("Mismatched server key type", expType, keyType);
-
-                KeyPair kp;
-                try {
-                    kp = ValidateUtils.checkNotNull(serverKeys.loadKey(null, keyType), "No server key for type=%s", keyType);
-                } catch (IOException | GeneralSecurityException e) {
-                    throw new RuntimeException(
-                            "Unexpected " + e.getClass().getSimpleName() + ")"
-                                               + " keys loading exception: " + e.getMessage(),
-                            e);
-                }
-                assertKeyEquals("Mismatched server public keys", kp.getPublic(), serverKey);
-                return true;
-            });
-
-            // allow only EC keys for public key authentication
-            org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory factory
-                    = new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory();
-            factory.setSignatureFactories(
-                    Arrays.asList(
-                            BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521));
-            client.setUserAuthFactories(Collections.singletonList(factory));
-
-            client.start();
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                s.addPublicKeyIdentity(clientIdentity);
-                s.auth().verify(AUTH_TIMEOUT);
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-624
-    public void testMismatchedUserAuthPkOkData() throws Exception {
-        AtomicInteger challengeCounter = new AtomicInteger(0);
-        sshd.setUserAuthFactories(Collections.singletonList(
-                new org.apache.sshd.server.auth.pubkey.UserAuthPublicKeyFactory() {
-                    @Override
-                    public org.apache.sshd.server.auth.pubkey.UserAuthPublicKey createUserAuth(ServerSession session)
-                            throws IOException {
-                        return new org.apache.sshd.server.auth.pubkey.UserAuthPublicKey() {
-                            @Override
-                            protected void sendPublicKeyResponse(
-                                    ServerSession session, String username, String alg, PublicKey key,
-                                    byte[] keyBlob, int offset, int blobLen, Buffer buffer)
-                                    throws Exception {
-                                int count = challengeCounter.incrementAndGet();
-                                outputDebugMessage("sendPublicKeyChallenge(%s)[%s]: count=%d", session, alg, count);
-                                if (count == 1) {
-                                    // send wrong key type
-                                    super.sendPublicKeyResponse(session, username,
-                                            KeyPairProvider.SSH_DSS, key, keyBlob, offset, blobLen, buffer);
-                                } else if (count == 2) {
-                                    // send another key
-                                    KeyPair otherPair = org.apache.sshd.util.test.CommonTestSupportUtils
-                                            .generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024);
-                                    PublicKey otherKey = otherPair.getPublic();
-                                    Buffer buf = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_PK_OK,
-                                            blobLen + alg.length() + Long.SIZE);
-                                    buf.putString(alg);
-                                    buf.putPublicKey(otherKey);
-                                    session.writePacket(buf);
-                                } else {
-                                    super.sendPublicKeyResponse(session, username, alg, key, keyBlob, offset, blobLen, buffer);
-                                }
-                            }
-                        };
-                    }
-
-                }));
-
-        try (SshClient client = setupTestClient()) {
-            KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(
-                    CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM,
-                    CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE);
-            client.start();
-
-            try {
-                for (int index = 1; index <= 4; index++) {
-                    try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                            .verify(CONNECT_TIMEOUT)
-                            .getSession()) {
-                        s.addPublicKeyIdentity(clientIdentity);
-                        s.auth().verify(AUTH_TIMEOUT);
-                        assertEquals("Mismatched number of challenges", 3, challengeCounter.get());
-                        break;
-                    } catch (SshException e) { // expected
-                        outputDebugMessage("%s on retry #%d: %s", e.getClass().getSimpleName(), index, e.getMessage());
-
-                        Throwable t = e.getCause();
-                        assertObjectInstanceOf("Unexpected failure cause at retry #" + index, InvalidKeySpecException.class, t);
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-620
-    public void testHostBasedAuthentication() throws Exception {
-        AtomicInteger invocationCount = new AtomicInteger(0);
-        testHostBasedAuthentication(
-                (
-                        session, username, clientHostKey, clientHostName, clientUsername,
-                        certificates) -> invocationCount.incrementAndGet() > 0,
-                session -> {
-                    /* ignored */ });
-        assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get());
-    }
-
-    @Test   // see SSHD-1114
-    public void testHostBasedAuthenticationReporter() throws Exception {
-        AtomicReference<String> hostnameClientHolder = new AtomicReference<>();
-        AtomicReference<String> usernameClientHolder = new AtomicReference<>();
-        AtomicReference<PublicKey> keyClientHolder = new AtomicReference<>();
-        HostBasedAuthenticator authenticator
-                = (session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
-                    return Objects.equals(clientHostName, hostnameClientHolder.get())
-                            && Objects.equals(clientUsername, usernameClientHolder.get())
-                            && KeyUtils.compareKeys(clientHostKey, keyClientHolder.get());
-                };
-
-        HostBasedAuthenticationReporter reporter = new HostBasedAuthenticationReporter() {
-            @Override
-            public void signalAuthenticationAttempt(
-                    ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature)
-                    throws Exception {
-                hostnameClientHolder.set(hostname);
-                usernameClientHolder.set(username);
-                keyClientHolder.set(identity.getPublic());
-            }
-
-            @Override
-            public void signalAuthenticationSuccess(
-                    ClientSession session, String service, KeyPair identity, String hostname, String username)
-                    throws Exception {
-                assertEquals("Host", hostname, hostnameClientHolder.get());
-                assertEquals("User", username, usernameClientHolder.get());
-                assertKeyEquals("Identity", identity.getPublic(), keyClientHolder.get());
-            }
-
-            @Override
-            public void signalAuthenticationFailure(
-                    ClientSession session, String service, KeyPair identity,
-                    String hostname, String username, boolean partial, List<String> serverMethods)
-                    throws Exception {
-                fail("Unexpected failure signalled");
-            }
-        };
-
-        testHostBasedAuthentication(authenticator, session -> session.setHostBasedAuthenticationReporter(reporter));
-    }
-
-    private void testHostBasedAuthentication(
-            HostBasedAuthenticator delegate, Consumer<? super ClientSession> preAuthInitializer)
-            throws Exception {
-        String hostClientUser = getClass().getSimpleName();
-        String hostClientName = SshdSocketAddress.toAddressString(SshdSocketAddress.getFirstExternalNetwork4Address());
-        KeyPair hostClientKey = CommonTestSupportUtils.generateKeyPair(
-                CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM,
-                CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE);
-        sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
-            return hostClientUser.equals(clientUsername)
-                    && hostClientName.equals(clientHostName)
-                    && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey)
-                    && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates);
-        });
-        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
-        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
-        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
-        sshd.setUserAuthFactories(
-                Collections.singletonList(
-                        org.apache.sshd.server.auth.hostbased.UserAuthHostBasedFactory.INSTANCE));
-
-        try (SshClient client = setupTestClient()) {
-            org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory
-                    = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory();
-            // TODO factory.setClientHostname(CLIENT_HOSTNAME);
-            factory.setClientUsername(hostClientUser);
-            factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey));
-
-            client.setUserAuthFactories(Collections.singletonList(factory));
-            client.start();
-            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                preAuthInitializer.accept(session);
-                session.auth().verify(AUTH_TIMEOUT);
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
     @Test // see SSHD-625
     public void testRuntimeErrorsInAuthenticators() throws Exception {
         Error thrown = new OutOfMemoryError(getCurrentTestName());
@@ -925,97 +151,6 @@ public class AuthenticationTest extends BaseTestSupport {
         }
     }
 
-    @Test // see SSHD-714
-    public void testPasswordIdentityWithSpacesPrefixOrSuffix() throws Exception {
-        sshd.setPasswordAuthenticator((username, password, session) -> {
-            return (username != null) && (!username.trim().isEmpty())
-                    && (password != null) && (!password.isEmpty())
-                    && ((password.charAt(0) == ' ') || (password.charAt(password.length() - 1) == ' '));
-        });
-
-        try (SshClient client = setupTestClient()) {
-            client.start();
-
-            try {
-                for (String password : new String[] {
-                        " ", "    ", "  " + getCurrentTestName(), getCurrentTestName() + "    "
-                }) {
-                    try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                            .verify(CONNECT_TIMEOUT)
-                            .getSession()) {
-                        s.addPasswordIdentity(password);
-
-                        AuthFuture auth = s.auth();
-                        assertTrue("No authentication result in time for password='" + password + "'",
-                                auth.await(AUTH_TIMEOUT));
-                        assertTrue("Failed to authenticate with password='" + password + "'", auth.isSuccess());
-                    }
-                }
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
-    @Test // see SSHD-862
-    public void testSessionContextPropagatedToKeyFilePasswordProvider() throws Exception {
-        try (SshClient client = setupTestClient()) {
-            client.start();
-
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT)
-                    .getSession()) {
-                String keyLocation = "super-secret-passphrase-ec256-key";
-                FilePasswordProvider passwordProvider = new FilePasswordProvider() {
-                    @Override
-                    @SuppressWarnings("synthetic-access")
-                    public String getPassword(
-                            SessionContext session, NamedResource resourceKey, int retryIndex)
-                            throws IOException {
-                        assertSame("Mismatched session context", s, session);
-                        assertEquals("Mismatched retry index", 0, retryIndex);
-
-                        String name = resourceKey.getName();
-                        int pos = name.lastIndexOf('/');
-                        if (pos >= 0) {
-                            name = name.substring(pos + 1);
-                        }
-                        assertEquals("Mismatched location", keyLocation, name);
-
-                        Boolean passwordRequested = session.getAttribute(PASSWORD_ATTR);
-                        assertNull("Password already requested", passwordRequested);
-                        session.setAttribute(PASSWORD_ATTR, Boolean.TRUE);
-                        return "super secret passphrase";
-                    }
-                };
-                s.setKeyIdentityProvider(new KeyIdentityProvider() {
-                    @Override
-                    public Iterable<KeyPair> loadKeys(SessionContext session) throws IOException, GeneralSecurityException {
-                        assertSame("Mismatched session context", s, session);
-                        URL location = getClass().getResource(keyLocation);
-                        assertNotNull("Missing key file " + keyLocation, location);
-
-                        URLResource resourceKey = new URLResource(location);
-                        Iterable<KeyPair> ids;
-                        try (InputStream keyData = resourceKey.openInputStream()) {
-                            ids = SecurityUtils.loadKeyPairIdentities(session, resourceKey, keyData, passwordProvider);
-                        }
-                        KeyPair kp = GenericUtils.head(ids);
-                        assertNotNull("No identity loaded from " + resourceKey, kp);
-                        return Collections.singletonList(kp);
-                    }
-                });
-                s.auth().verify(AUTH_TIMEOUT);
-
-                Boolean passwordRequested = s.getAttribute(PASSWORD_ATTR);
-                assertNotNull("Password provider not invoked", passwordRequested);
-                assertTrue("Password not requested", passwordRequested.booleanValue());
-            } finally {
-                client.stop();
-            }
-        }
-    }
-
     @Test   // see SSHD-1040
     public void testServerKeyAvailableAfterAuth() throws Exception {
         KeyPairProvider keyPairProvider = sshd.getKeyPairProvider();
@@ -1050,183 +185,4 @@ public class AuthenticationTest extends BaseTestSupport {
 
         fail("No matching server key found for " + actualKey);
     }
-
-    @Test   // see SSHD-1114
-    public void testPasswordAuthenticationReporter() throws Exception {
-        String goodPassword = getCurrentTestName();
-        String badPassword = getClass().getSimpleName();
-        List<String> attempted = new ArrayList<>();
-        sshd.setPasswordAuthenticator((user, password, session) -> {
-            attempted.add(password);
-            return goodPassword.equals(password);
-        });
-        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
-        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
-
-        List<String> reported = new ArrayList<>();
-        PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() {
-            @Override
-            public void signalAuthenticationAttempt(
-                    ClientSession session, String service, String oldPassword, boolean modified, String newPassword)
-                    throws Exception {
-                reported.add(oldPassword);
-            }
-
-            @Override
-            public void signalAuthenticationSuccess(ClientSession session, String service, String password)
-                    throws Exception {
-                assertEquals("Mismatched succesful password", goodPassword, password);
-            }
-
-            @Override
-            public void signalAuthenticationFailure(
-                    ClientSession session, String service, String password, boolean partial, List<String> serverMethods)
-                    throws Exception {
-                assertEquals("Mismatched failed password", badPassword, password);
-            }
-        };
-
-        try (SshClient client = setupTestClient()) {
-            client.setUserAuthFactories(
-                    Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory()));
-            client.start();
-
-            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT).getSession()) {
-                session.addPasswordIdentity(badPassword);
-                session.addPasswordIdentity(goodPassword);
-                session.setPasswordAuthenticationReporter(reporter);
-                session.auth().verify(AUTH_TIMEOUT);
-            } finally {
-                client.stop();
-            }
-        }
-
-        List<String> expected = Arrays.asList(badPassword, goodPassword);
-        assertListEquals("Attempted passwords", expected, attempted);
-        assertListEquals("Reported passwords", expected, reported);
-    }
-
-    @Test   // see SSHD-1114
-    public void testPublicKeyAuthenticationReporter() throws Exception {
-        KeyPair goodIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
-        KeyPair badIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
-        List<PublicKey> attempted = new ArrayList<>();
-        sshd.setPublickeyAuthenticator((username, key, session) -> {
-            attempted.add(key);
-            return KeyUtils.compareKeys(goodIdentity.getPublic(), key);
-        });
-        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
-        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
-
-        List<PublicKey> reported = new ArrayList<>();
-        List<PublicKey> signed = new ArrayList<>();
-        PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() {
-            @Override
-            public void signalAuthenticationAttempt(
-                    ClientSession session, String service, KeyPair identity, String signature)
-                    throws Exception {
-                reported.add(identity.getPublic());
-            }
-
-            @Override
-            public void signalSignatureAttempt(
-                    ClientSession session, String service, KeyPair identity, String signature, byte[] sigData)
-                    throws Exception {
-                signed.add(identity.getPublic());
-            }
-
-            @Override
-            public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity)
-                    throws Exception {
-                assertTrue("Mismatched success identity", KeyUtils.compareKeys(goodIdentity.getPublic(), identity.getPublic()));
-            }
-
-            @Override
-            public void signalAuthenticationFailure(
-                    ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods)
-                    throws Exception {
-                assertTrue("Mismatched failed identity", KeyUtils.compareKeys(badIdentity.getPublic(), identity.getPublic()));
-            }
-        };
-
-        try (SshClient client = setupTestClient()) {
-            client.setUserAuthFactories(
-                    Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory()));
-            client.start();
-
-            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
-                    .verify(CONNECT_TIMEOUT).getSession()) {
-                session.addPublicKeyIdentity(badIdentity);
-                session.addPublicKeyIdentity(goodIdentity);
-                session.setPublicKeyAuthenticationReporter(reporter);
-                session.auth().verify(AUTH_TIMEOUT);
-            } finally {
-                client.stop();
-            }
-        }
-
-        List<PublicKey> expected = Arrays.asList(badIdentity.getPublic(), goodIdentity.getPublic());
-        // The server public key authenticator is called twice with the good identity
-        int numAttempted = attempted.size();
-        assertKeyListEquals("Attempted", expected, (numAttempted > 0) ? attempted.subList(0, numAttempted - 1) : attempted);
-        assertKeyListEquals("Reported", expected, reported);
-        // The signing is attempted only if the initial public key is accepted
-        assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed);
-    }
-
-    private static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException {
-        assertTrue(message + ": failed to get result on time", future.await(AUTH_TIMEOUT));
-        assertEquals(message + ": mismatched authentication result", expected, future.isSuccess());
-    }
-
-    private static AuthFuture authPassword(ClientSession s, String user, String pswd) throws IOException {
-        s.setUsername(user);
-        s.addPasswordIdentity(pswd);
-        return s.auth();
-    }
-
-    private static AuthFuture authInteractive(ClientSession s, String user, String pswd) throws IOException {
-        s.setUsername(user);
-        final String[] response = { pswd };
-        s.setUserInteraction(new UserInteraction() {
-            @Override
-            public boolean isInteractionAllowed(ClientSession session) {
-                return true;
-            }
-
-            @Override
-            public String[] interactive(
-                    ClientSession session, String name, String instruction,
-                    String lang, String[] prompt, boolean[] echo) {
-                assertSame("Mismatched session instance", s, session);
-                assertEquals("Mismatched prompt size", 1, GenericUtils.length(prompt));
-                assertTrue("Mismatched prompt: " + prompt[0], prompt[0].toLowerCase().contains("password"));
-                return response;
-            }
-
-            @Override
-            public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
-                throw new UnsupportedOperationException("Unexpected password update request");
-            }
-        });
-        return s.auth();
-    }
-
-    private static AuthFuture authPublicKey(ClientSession s, String user, KeyPair pair) throws IOException {
-        s.setUsername(user);
-        s.addPublicKeyIdentity(pair);
-        return s.auth();
-    }
-
-    public static class TestSession extends ServerSessionImpl {
-        public TestSession(ServerFactoryManager server, IoSession ioSession) throws Exception {
-            super(server, ioSession);
-        }
-
-        @Override
-        public void handleMessage(Buffer buffer) throws Exception {
-            super.handleMessage(buffer); // debug breakpoint
-        }
-    }
 }
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java
new file mode 100644
index 0000000..3fab5de
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java
@@ -0,0 +1,105 @@
+/*
+ * 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.common.auth;
+
+import java.io.IOException;
+import java.security.KeyPair;
+
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AuthenticationTestSupport extends BaseTestSupport {
+    protected static final AttributeRepository.AttributeKey<Boolean> PASSWORD_ATTR = new AttributeRepository.AttributeKey<>();
+
+    protected SshServer sshd;
+    protected int port;
+
+    protected AuthenticationTestSupport() {
+        super();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd = setupTestServer();
+        sshd.start();
+        port = sshd.getPort();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            sshd.stop(true);
+        }
+    }
+
+    protected static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException {
+        assertTrue(message + ": failed to get result on time", future.await(AUTH_TIMEOUT));
+        assertEquals(message + ": mismatched authentication result", expected, future.isSuccess());
+    }
+
+    protected static AuthFuture authPassword(ClientSession s, String user, String pswd) throws IOException {
+        s.setUsername(user);
+        s.addPasswordIdentity(pswd);
+        return s.auth();
+    }
+
+    protected static AuthFuture authInteractive(ClientSession s, String user, String pswd) throws IOException {
+        s.setUsername(user);
+        final String[] response = { pswd };
+        s.setUserInteraction(new UserInteraction() {
+            @Override
+            public boolean isInteractionAllowed(ClientSession session) {
+                return true;
+            }
+
+            @Override
+            public String[] interactive(
+                    ClientSession session, String name, String instruction,
+                    String lang, String[] prompt, boolean[] echo) {
+                assertSame("Mismatched session instance", s, session);
+                assertEquals("Mismatched prompt size", 1, GenericUtils.length(prompt));
+                assertTrue("Mismatched prompt: " + prompt[0], prompt[0].toLowerCase().contains("password"));
+                return response;
+            }
+
+            @Override
+            public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                throw new UnsupportedOperationException("Unexpected password update request");
+            }
+        });
+        return s.auth();
+    }
+
+    protected static AuthFuture authPublicKey(ClientSession s, String user, KeyPair pair) throws IOException {
+        s.setUsername(user);
+        s.addPublicKeyIdentity(pair);
+        return s.auth();
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java
new file mode 100644
index 0000000..7568bc8
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.common.auth;
+
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
+import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;
+import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
+import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class HostBasedAuthenticationTest extends AuthenticationTestSupport {
+    public HostBasedAuthenticationTest() {
+        super();
+    }
+
+    @Test // see SSHD-620
+    public void testHostBasedAuthentication() throws Exception {
+        AtomicInteger invocationCount = new AtomicInteger(0);
+        testHostBasedAuthentication(
+                (
+                        session, username, clientHostKey, clientHostName, clientUsername,
+                        certificates) -> invocationCount.incrementAndGet() > 0,
+                session -> {
+                    /* ignored */ });
+        assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get());
+    }
+
+    @Test   // see SSHD-1114
+    public void testHostBasedAuthenticationReporter() throws Exception {
+        AtomicReference<String> hostnameClientHolder = new AtomicReference<>();
+        AtomicReference<String> usernameClientHolder = new AtomicReference<>();
+        AtomicReference<PublicKey> keyClientHolder = new AtomicReference<>();
+        HostBasedAuthenticator authenticator
+                = (session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
+                    return Objects.equals(clientHostName, hostnameClientHolder.get())
+                            && Objects.equals(clientUsername, usernameClientHolder.get())
+                            && KeyUtils.compareKeys(clientHostKey, keyClientHolder.get());
+                };
+
+        HostBasedAuthenticationReporter reporter = new HostBasedAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature)
+                    throws Exception {
+                hostnameClientHolder.set(hostname);
+                usernameClientHolder.set(username);
+                keyClientHolder.set(identity.getPublic());
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(
+                    ClientSession session, String service, KeyPair identity, String hostname, String username)
+                    throws Exception {
+                assertEquals("Host", hostname, hostnameClientHolder.get());
+                assertEquals("User", username, usernameClientHolder.get());
+                assertKeyEquals("Identity", identity.getPublic(), keyClientHolder.get());
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, KeyPair identity,
+                    String hostname, String username, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                fail("Unexpected failure signalled");
+            }
+        };
+
+        testHostBasedAuthentication(authenticator, session -> session.setHostBasedAuthenticationReporter(reporter));
+    }
+
+    private void testHostBasedAuthentication(
+            HostBasedAuthenticator delegate, Consumer<? super ClientSession> preAuthInitializer)
+            throws Exception {
+        String hostClientUser = getClass().getSimpleName();
+        String hostClientName = SshdSocketAddress.toAddressString(SshdSocketAddress.getFirstExternalNetwork4Address());
+        KeyPair hostClientKey = CommonTestSupportUtils.generateKeyPair(
+                CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM,
+                CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE);
+        sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
+            return hostClientUser.equals(clientUsername)
+                    && hostClientName.equals(clientHostName)
+                    && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey)
+                    && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates);
+        });
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+        sshd.setUserAuthFactories(
+                Collections.singletonList(
+                        org.apache.sshd.server.auth.hostbased.UserAuthHostBasedFactory.INSTANCE));
+
+        try (SshClient client = setupTestClient()) {
+            org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory
+                    = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory();
+            // TODO factory.setClientHostname(CLIENT_HOSTNAME);
+            factory.setClientUsername(hostClientUser);
+            factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey));
+
+            client.setUserAuthFactories(Collections.singletonList(factory));
+            client.start();
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                preAuthInitializer.accept(session);
+                session.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java
new file mode 100644
index 0000000..feeabf3
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.common.auth;
+
+import java.security.KeyPair;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
+import org.apache.sshd.server.auth.keyboard.InteractiveChallenge;
+import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
+import org.apache.sshd.server.auth.keyboard.PromptEntry;
+import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class KeyboardInteractiveAuthenticationTest extends AuthenticationTestSupport {
+    public KeyboardInteractiveAuthenticationTest() {
+        super();
+    }
+
+    @Test // see SSHD-612
+    public void testAuthDefaultKeyInteractive() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+            sshd.setKeyboardInteractiveAuthenticator(new DefaultKeyboardInteractiveAuthenticator() {
+                @Override
+                public InteractiveChallenge generateChallenge(
+                        ServerSession session, String username, String lang, String subMethods)
+                        throws Exception {
+                    assertEquals("Mismatched user language",
+                            CoreModuleProperties.INTERACTIVE_LANGUAGE_TAG.getRequired(client),
+                            lang);
+                    assertEquals("Mismatched client sub-methods",
+                            CoreModuleProperties.INTERACTIVE_SUBMETHODS.getRequired(client),
+                            subMethods);
+
+                    InteractiveChallenge challenge = super.generateChallenge(session, username, lang, subMethods);
+                    assertEquals("Mismatched interaction name", getInteractionName(session), challenge.getInteractionName());
+                    assertEquals("Mismatched interaction instruction", getInteractionInstruction(session),
+                            challenge.getInteractionInstruction());
+                    assertEquals("Mismatched language tag", getInteractionLanguage(session), challenge.getLanguageTag());
+
+                    List<PromptEntry> entries = challenge.getPrompts();
+                    assertEquals("Mismatched prompts count", 1, GenericUtils.size(entries));
+
+                    PromptEntry entry = entries.get(0);
+                    assertEquals("Mismatched prompt", getInteractionPrompt(session), entry.getPrompt());
+                    assertEquals("Mismatched echo", isInteractionPromptEchoEnabled(session), entry.isEcho());
+
+                    return challenge;
+                }
+
+                @Override
+                public boolean authenticate(
+                        ServerSession session, String username, List<String> responses)
+                        throws Exception {
+                    return super.authenticate(session, username, responses);
+                }
+
+            });
+            client.start();
+
+            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
+                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
+                        DEFAULT_TIMEOUT);
+                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
+
+                KeyPairProvider provider = createTestHostKeyProvider();
+                KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE);
+                try {
+                    assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY,
+                            authPublicKey(s, getCurrentTestName(), pair), false);
+                } finally {
+                    s.removePublicKeyIdentity(pair);
+                }
+
+                try {
+                    assertAuthenticationResult(UserAuthMethodFactory.KB_INTERACTIVE,
+                            authInteractive(s, getCurrentTestName(), getCurrentTestName()), true);
+                } finally {
+                    s.setUserInteraction(null);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-563
+    public void testAuthMultiChallengeKeyInteractive() throws Exception {
+        Class<?> anchor = getClass();
+        InteractiveChallenge challenge = new InteractiveChallenge();
+        challenge.setInteractionName(getCurrentTestName());
+        challenge.setInteractionInstruction(anchor.getPackage().getName());
+        challenge.setLanguageTag(Locale.getDefault().getLanguage());
+
+        Map<String, String> rspMap = NavigableMapBuilder.<String, String> builder(String.CASE_INSENSITIVE_ORDER)
+                .put("class", anchor.getSimpleName())
+                .put("package", anchor.getPackage().getName())
+                .put("test", getCurrentTestName())
+                .build();
+        for (String prompt : rspMap.keySet()) {
+            challenge.addPrompt(prompt, (GenericUtils.size(challenge.getPrompts()) & 0x1) != 0);
+        }
+
+        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME);
+        AtomicInteger genCount = new AtomicInteger(0);
+        AtomicInteger authCount = new AtomicInteger(0);
+        sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() {
+            @Override
+            public InteractiveChallenge generateChallenge(
+                    ServerSession session, String username, String lang, String subMethods)
+                    throws Exception {
+                assertEquals("Unexpected challenge call", 1, genCount.incrementAndGet());
+                return challenge;
+            }
+
+            @Override
+            public boolean authenticate(
+                    ServerSession session, String username, List<String> responses)
+                    throws Exception {
+                assertEquals("Unexpected authenticate call", 1, authCount.incrementAndGet());
+                assertEquals("Mismatched number of responses", GenericUtils.size(rspMap), GenericUtils.size(responses));
+
+                int index = 0;
+                // Cannot use forEach because the index is not effectively final
+                for (Map.Entry<String, String> re : rspMap.entrySet()) {
+                    String prompt = re.getKey();
+                    String expected = re.getValue();
+                    String actual = responses.get(index);
+                    assertEquals("Mismatched response for prompt=" + prompt, expected, actual);
+                    index++;
+                }
+                return true;
+            }
+        });
+        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME);
+
+        try (SshClient client = setupTestClient()) {
+            AtomicInteger interactiveCount = new AtomicInteger(0);
+            client.setUserInteraction(new UserInteraction() {
+                @Override
+                public boolean isInteractionAllowed(ClientSession session) {
+                    return true;
+                }
+
+                @Override
+                public String[] interactive(
+                        ClientSession session, String name, String instruction,
+                        String lang, String[] prompt, boolean[] echo) {
+                    assertEquals("Unexpected multiple calls", 1, interactiveCount.incrementAndGet());
+                    assertEquals("Mismatched name", challenge.getInteractionName(), name);
+                    assertEquals("Mismatched instruction", challenge.getInteractionInstruction(), instruction);
+                    assertEquals("Mismatched language", challenge.getLanguageTag(), lang);
+
+                    List<PromptEntry> entries = challenge.getPrompts();
+                    assertEquals("Mismatched prompts count", GenericUtils.size(entries), GenericUtils.length(prompt));
+
+                    String[] responses = new String[prompt.length];
+                    for (int index = 0; index < prompt.length; index++) {
+                        PromptEntry e = entries.get(index);
+                        String key = e.getPrompt();
+                        assertEquals("Mismatched prompt at index=" + index, key, prompt[index]);
+                        assertEquals("Mismatched echo at index=" + index, e.isEcho(), echo[index]);
+                        responses[index] = ValidateUtils.checkNotNull(rspMap.get(key), "No value for prompt=%s", key);
+                    }
+
+                    return responses;
+                }
+
+                @Override
+                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                    throw new UnsupportedOperationException("Unexpected call");
+                }
+            });
+            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthKeyboardInteractiveFactory.NAME);
+
+            client.start();
+
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.auth().verify(AUTH_TIMEOUT);
+                assertEquals("Bad generated challenge count", 1, genCount.get());
+                assertEquals("Bad authentication count", 1, authCount.get());
+                assertEquals("Bad interactive count", 1, interactiveCount.get());
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
new file mode 100644
index 0000000..628338d
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
@@ -0,0 +1,440 @@
+/*
+ * 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.common.auth;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
+import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
+import org.apache.sshd.server.auth.password.PasswordAuthenticator;
+import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
+import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
+import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class PasswordAuthenticationTest extends AuthenticationTestSupport {
+    public PasswordAuthenticationTest() {
+        super();
+    }
+
+    @Test
+    public void testWrongPassword() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            client.start();
+            try (ClientSession s = client.connect("user", TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.addPasswordIdentity("bad password");
+                assertAuthenticationResult(getCurrentTestName(), s.auth(), false);
+            }
+        }
+    }
+
+    @Test
+    public void testChangeUser() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                Collection<ClientSession.ClientSessionEvent> mask
+                        = EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH);
+                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(mask, DEFAULT_TIMEOUT);
+                assertFalse("Timeout while waiting on session events",
+                        result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
+
+                String password = "the-password";
+                for (String username : new String[] { "user1", "user2" }) {
+                    try {
+                        assertAuthenticationResult(username, authPassword(s, username, password), false);
+                    } finally {
+                        s.removePasswordIdentity(password);
+                    }
+                }
+
+                // Note that WAIT_AUTH flag should be false, but since the internal
+                // authentication future is not updated, it's still returned
+                result = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), DEFAULT_TIMEOUT);
+                assertTrue("Mismatched client session close mask: " + result, result.containsAll(mask));
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-196
+    public void testChangePassword() throws Exception {
+        PasswordAuthenticator delegate = sshd.getPasswordAuthenticator();
+        AtomicInteger attemptsCount = new AtomicInteger(0);
+        AtomicInteger changesCount = new AtomicInteger(0);
+        sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
+            @Override
+            public boolean authenticate(String username, String password, ServerSession session) {
+                if (attemptsCount.incrementAndGet() == 1) {
+                    throw new PasswordChangeRequiredException(
+                            attemptsCount.toString(),
+                            getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault());
+                }
+
+                return delegate.authenticate(username, password, session);
+            }
+
+            @Override
+            public boolean handleClientPasswordChangeRequest(
+                    ServerSession session, String username, String oldPassword, String newPassword) {
+                if (changesCount.incrementAndGet() == 1) {
+                    assertNotEquals("Non-different passwords", oldPassword, newPassword);
+                    return authenticate(username, newPassword, session);
+                } else {
+                    return PasswordAuthenticator.super.handleClientPasswordChangeRequest(
+                            session, username, oldPassword, newPassword);
+                }
+            }
+        });
+        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME);
+
+        try (SshClient client = setupTestClient()) {
+            AtomicInteger updatesCount = new AtomicInteger(0);
+            client.setUserInteraction(new UserInteraction() {
+                @Override
+                public boolean isInteractionAllowed(ClientSession session) {
+                    return true;
+                }
+
+                @Override
+                public String[] interactive(
+                        ClientSession session, String name, String instruction,
+                        String lang, String[] prompt, boolean[] echo) {
+                    throw new UnsupportedOperationException("Unexpected call");
+                }
+
+                @Override
+                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                    assertEquals("Mismatched prompt", getCurrentTestName(), prompt);
+                    assertEquals("Mismatched language",
+                            CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang);
+                    assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet());
+                    return getCurrentTestName();
+                }
+            });
+
+            AtomicInteger sentCount = new AtomicInteger(0);
+            client.setUserAuthFactories(Collections.singletonList(
+                    new org.apache.sshd.client.auth.password.UserAuthPasswordFactory() {
+                        @Override
+                        public org.apache.sshd.client.auth.password.UserAuthPassword createUserAuth(ClientSession session)
+                                throws IOException {
+                            return new org.apache.sshd.client.auth.password.UserAuthPassword() {
+                                @Override
+                                protected IoWriteFuture sendPassword(
+                                        Buffer buffer, ClientSession session, String oldPassword, String newPassword)
+                                        throws Exception {
+                                    int count = sentCount.incrementAndGet();
+                                    // 1st one is the original one (which is denied by the server)
+                                    // 2nd one is the updated one retrieved from the user interaction
+                                    if (count == 2) {
+                                        return super.sendPassword(buffer, session, getClass().getName(), newPassword);
+                                    } else {
+                                        return super.sendPassword(buffer, session, oldPassword, newPassword);
+                                    }
+                                }
+                            };
+                        }
+                    }));
+            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME);
+
+            client.start();
+
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.addPasswordIdentity(getCurrentTestName());
+                s.auth().verify(AUTH_TIMEOUT);
+                assertEquals("No password change request generated", 2, attemptsCount.get());
+                assertEquals("No password change handled", 1, changesCount.get());
+                assertEquals("No user interaction invoked", 1, updatesCount.get());
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testAuthPasswordOnly() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+
+            client.start();
+            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
+                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
+                        DEFAULT_TIMEOUT);
+                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
+
+                String password = getCurrentTestName();
+                try {
+                    assertAuthenticationResult(getCurrentTestName(),
+                            authPassword(s, getCurrentTestName(), password), false);
+                } finally {
+                    s.removePasswordIdentity(password);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testAuthKeyPassword() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+            sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+
+            client.start();
+
+            try (ClientSession s = client.connect(null, TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                Collection<ClientSession.ClientSessionEvent> result = s.waitFor(
+                        EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH),
+                        DEFAULT_TIMEOUT);
+                assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT));
+
+                KeyPairProvider provider = createTestHostKeyProvider();
+                KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE);
+                try {
+                    assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY,
+                            authPublicKey(s, getCurrentTestName(), pair), false);
+                } finally {
+                    s.removePublicKeyIdentity(pair);
+                }
+
+                String password = getCurrentTestName();
+                try {
+                    assertAuthenticationResult(UserAuthMethodFactory.PASSWORD,
+                            authPassword(s, getCurrentTestName(), password), true);
+                } finally {
+                    s.removePasswordIdentity(password);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-196
+    public void testAuthPasswordChangeRequest() throws Exception {
+        PasswordAuthenticator delegate = Objects.requireNonNull(sshd.getPasswordAuthenticator(), "No password authenticator");
+        AtomicInteger attemptsCount = new AtomicInteger(0);
+        sshd.setPasswordAuthenticator((username, password, session) -> {
+            if (attemptsCount.incrementAndGet() == 1) {
+                throw new PasswordChangeRequiredException(
+                        attemptsCount.toString(),
+                        getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault());
+            }
+
+            return delegate.authenticate(username, password, session);
+        });
+        CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME);
+
+        try (SshClient client = setupTestClient()) {
+            AtomicInteger updatesCount = new AtomicInteger(0);
+            client.setUserInteraction(new UserInteraction() {
+                @Override
+                public boolean isInteractionAllowed(ClientSession session) {
+                    return true;
+                }
+
+                @Override
+                public String[] interactive(
+                        ClientSession session, String name, String instruction,
+                        String lang, String[] prompt, boolean[] echo) {
+                    throw new UnsupportedOperationException("Unexpected call");
+                }
+
+                @Override
+                public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                    assertEquals("Mismatched prompt", getCurrentTestName(), prompt);
+                    assertEquals("Mismatched language",
+                            CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang);
+                    assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet());
+                    return getCurrentTestName();
+                }
+            });
+            CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME);
+
+            client.start();
+
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.addPasswordIdentity(getCurrentTestName());
+                s.auth().verify(AUTH_TIMEOUT);
+                assertEquals("No password change request generated", 2, attemptsCount.get());
+                assertEquals("No user interaction invoked", 1, updatesCount.get());
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testPasswordIdentityProviderPropagation() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            List<String> passwords = Collections.singletonList(getCurrentTestName());
+            AtomicInteger loadCount = new AtomicInteger(0);
+            PasswordIdentityProvider provider = () -> {
+                loadCount.incrementAndGet();
+                outputDebugMessage("loadPasswords - count=%s", loadCount);
+                return passwords;
+            };
+            client.setPasswordIdentityProvider(provider);
+
+            client.start();
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.auth().verify(AUTH_TIMEOUT);
+                assertEquals("Mismatched load passwords count", 1, loadCount.get());
+                assertSame("Mismatched passwords identity provider", provider, s.getPasswordIdentityProvider());
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-714
+    public void testPasswordIdentityWithSpacesPrefixOrSuffix() throws Exception {
+        sshd.setPasswordAuthenticator((username, password, session) -> {
+            return (username != null) && (!username.trim().isEmpty())
+                    && (password != null) && (!password.isEmpty())
+                    && ((password.charAt(0) == ' ') || (password.charAt(password.length() - 1) == ' '));
+        });
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try {
+                for (String password : new String[] {
+                        " ", "    ", "  " + getCurrentTestName(), getCurrentTestName() + "    "
+                }) {
+                    try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                            .verify(CONNECT_TIMEOUT)
+                            .getSession()) {
+                        s.addPasswordIdentity(password);
+
+                        AuthFuture auth = s.auth();
+                        assertTrue("No authentication result in time for password='" + password + "'",
+                                auth.await(AUTH_TIMEOUT));
+                        assertTrue("Failed to authenticate with password='" + password + "'", auth.isSuccess());
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test   // see SSHD-1114
+    public void testPasswordAuthenticationReporter() throws Exception {
+        String goodPassword = getCurrentTestName();
+        String badPassword = getClass().getSimpleName();
+        List<String> attempted = new ArrayList<>();
+        sshd.setPasswordAuthenticator((user, password, session) -> {
+            attempted.add(password);
+            return goodPassword.equals(password);
+        });
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+
+        List<String> reported = new ArrayList<>();
+        PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, String oldPassword, boolean modified, String newPassword)
+                    throws Exception {
+                reported.add(oldPassword);
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(ClientSession session, String service, String password)
+                    throws Exception {
+                assertEquals("Mismatched succesful password", goodPassword, password);
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, String password, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                assertEquals("Mismatched failed password", badPassword, password);
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.addPasswordIdentity(badPassword);
+                session.addPasswordIdentity(goodPassword);
+                session.setPasswordAuthenticationReporter(reporter);
+                session.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+
+        List<String> expected = Arrays.asList(badPassword, goodPassword);
+        assertListEquals("Attempted passwords", expected, attempted);
+        assertListEquals("Reported passwords", expected, reported);
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
new file mode 100644
index 0000000..300b77e
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.common.auth;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.io.resource.URLResource;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
+import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.apache.sshd.util.test.CoreTestSupportUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class PublicKeyAuthenticationTest extends AuthenticationTestSupport {
+    public PublicKeyAuthenticationTest() {
+        super();
+    }
+
+    @Test // see SSHD-618
+    public void testPublicKeyAuthDifferentThanKex() throws Exception {
+        KeyPairProvider serverKeys = KeyPairProvider.wrap(
+                CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024),
+                CommonTestSupportUtils.generateKeyPair(KeyUtils.DSS_ALGORITHM, 512),
+                CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256));
+        sshd.setKeyPairProvider(serverKeys);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+
+        KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        sshd.setPublickeyAuthenticator((username, key, session) -> {
+            String keyType = KeyUtils.getKeyType(key);
+            String expType = KeyUtils.getKeyType(clientIdentity);
+            assertEquals("Mismatched client key types", expType, keyType);
+            assertKeyEquals("Mismatched authentication public keys", clientIdentity.getPublic(), key);
+            return true;
+        });
+
+        // since we need to use RSA
+        CoreTestSupportUtils.setupFullSignaturesSupport(sshd);
+        try (SshClient client = setupTestClient()) {
+            // force server to use only RSA
+            NamedFactory<Signature> kexSignature = BuiltinSignatures.rsa;
+            client.setSignatureFactories(Collections.singletonList(kexSignature));
+            client.setServerKeyVerifier((sshClientSession, remoteAddress, serverKey) -> {
+                String keyType = KeyUtils.getKeyType(serverKey);
+                String expType = kexSignature.getName();
+                assertEquals("Mismatched server key type", expType, keyType);
+
+                KeyPair kp;
+                try {
+                    kp = ValidateUtils.checkNotNull(serverKeys.loadKey(null, keyType), "No server key for type=%s", keyType);
+                } catch (IOException | GeneralSecurityException e) {
+                    throw new RuntimeException(
+                            "Unexpected " + e.getClass().getSimpleName() + ")"
+                                               + " keys loading exception: " + e.getMessage(),
+                            e);
+                }
+                assertKeyEquals("Mismatched server public keys", kp.getPublic(), serverKey);
+                return true;
+            });
+
+            // allow only EC keys for public key authentication
+            org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory factory
+                    = new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory();
+            factory.setSignatureFactories(
+                    Arrays.asList(
+                            BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521));
+            client.setUserAuthFactories(Collections.singletonList(factory));
+
+            client.start();
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                s.addPublicKeyIdentity(clientIdentity);
+                s.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-624
+    public void testMismatchedUserAuthPkOkData() throws Exception {
+        AtomicInteger challengeCounter = new AtomicInteger(0);
+        sshd.setUserAuthFactories(Collections.singletonList(
+                new org.apache.sshd.server.auth.pubkey.UserAuthPublicKeyFactory() {
+                    @Override
+                    public org.apache.sshd.server.auth.pubkey.UserAuthPublicKey createUserAuth(ServerSession session)
+                            throws IOException {
+                        return new org.apache.sshd.server.auth.pubkey.UserAuthPublicKey() {
+                            @Override
+                            protected void sendPublicKeyResponse(
+                                    ServerSession session, String username, String alg, PublicKey key,
+                                    byte[] keyBlob, int offset, int blobLen, Buffer buffer)
+                                    throws Exception {
+                                int count = challengeCounter.incrementAndGet();
+                                outputDebugMessage("sendPublicKeyChallenge(%s)[%s]: count=%d", session, alg, count);
+                                if (count == 1) {
+                                    // send wrong key type
+                                    super.sendPublicKeyResponse(session, username,
+                                            KeyPairProvider.SSH_DSS, key, keyBlob, offset, blobLen, buffer);
+                                } else if (count == 2) {
+                                    // send another key
+                                    KeyPair otherPair = org.apache.sshd.util.test.CommonTestSupportUtils
+                                            .generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024);
+                                    PublicKey otherKey = otherPair.getPublic();
+                                    Buffer buf = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_PK_OK,
+                                            blobLen + alg.length() + Long.SIZE);
+                                    buf.putString(alg);
+                                    buf.putPublicKey(otherKey);
+                                    session.writePacket(buf);
+                                } else {
+                                    super.sendPublicKeyResponse(session, username, alg, key, keyBlob, offset, blobLen, buffer);
+                                }
+                            }
+                        };
+                    }
+
+                }));
+
+        try (SshClient client = setupTestClient()) {
+            KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(
+                    CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM,
+                    CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE);
+            client.start();
+
+            try {
+                for (int index = 1; index <= 4; index++) {
+                    try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                            .verify(CONNECT_TIMEOUT)
+                            .getSession()) {
+                        s.addPublicKeyIdentity(clientIdentity);
+                        s.auth().verify(AUTH_TIMEOUT);
+                        assertEquals("Mismatched number of challenges", 3, challengeCounter.get());
+                        break;
+                    } catch (SshException e) { // expected
+                        outputDebugMessage("%s on retry #%d: %s", e.getClass().getSimpleName(), index, e.getMessage());
+
+                        Throwable t = e.getCause();
+                        assertObjectInstanceOf("Unexpected failure cause at retry #" + index, InvalidKeySpecException.class, t);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test // see SSHD-862
+    public void testSessionContextPropagatedToKeyFilePasswordProvider() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT)
+                    .getSession()) {
+                String keyLocation = "super-secret-passphrase-ec256-key";
+                FilePasswordProvider passwordProvider = new FilePasswordProvider() {
+                    @Override
+                    public String getPassword(
+                            SessionContext session, NamedResource resourceKey, int retryIndex)
+                            throws IOException {
+                        assertSame("Mismatched session context", s, session);
+                        assertEquals("Mismatched retry index", 0, retryIndex);
+
+                        String name = resourceKey.getName();
+                        int pos = name.lastIndexOf('/');
+                        if (pos >= 0) {
+                            name = name.substring(pos + 1);
+                        }
+                        assertEquals("Mismatched location", keyLocation, name);
+
+                        Boolean passwordRequested = session.getAttribute(PASSWORD_ATTR);
+                        assertNull("Password already requested", passwordRequested);
+                        session.setAttribute(PASSWORD_ATTR, Boolean.TRUE);
+                        return "super secret passphrase";
+                    }
+                };
+                s.setKeyIdentityProvider(new KeyIdentityProvider() {
+                    @Override
+                    public Iterable<KeyPair> loadKeys(SessionContext session) throws IOException, GeneralSecurityException {
+                        assertSame("Mismatched session context", s, session);
+                        URL location = getClass().getResource(keyLocation);
+                        assertNotNull("Missing key file " + keyLocation, location);
+
+                        URLResource resourceKey = new URLResource(location);
+                        Iterable<KeyPair> ids;
+                        try (InputStream keyData = resourceKey.openInputStream()) {
+                            ids = SecurityUtils.loadKeyPairIdentities(session, resourceKey, keyData, passwordProvider);
+                        }
+                        KeyPair kp = GenericUtils.head(ids);
+                        assertNotNull("No identity loaded from " + resourceKey, kp);
+                        return Collections.singletonList(kp);
+                    }
+                });
+                s.auth().verify(AUTH_TIMEOUT);
+
+                Boolean passwordRequested = s.getAttribute(PASSWORD_ATTR);
+                assertNotNull("Password provider not invoked", passwordRequested);
+                assertTrue("Password not requested", passwordRequested.booleanValue());
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test   // see SSHD-1114
+    public void testPublicKeyAuthenticationReporter() throws Exception {
+        KeyPair goodIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        KeyPair badIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        List<PublicKey> attempted = new ArrayList<>();
+        sshd.setPublickeyAuthenticator((username, key, session) -> {
+            attempted.add(key);
+            return KeyUtils.compareKeys(goodIdentity.getPublic(), key);
+        });
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+
+        List<PublicKey> reported = new ArrayList<>();
+        List<PublicKey> signed = new ArrayList<>();
+        PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, KeyPair identity, String signature)
+                    throws Exception {
+                reported.add(identity.getPublic());
+            }
+
+            @Override
+            public void signalSignatureAttempt(
+                    ClientSession session, String service, KeyPair identity, String signature, byte[] sigData)
+                    throws Exception {
+                signed.add(identity.getPublic());
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity)
+                    throws Exception {
+                assertTrue("Mismatched success identity", KeyUtils.compareKeys(goodIdentity.getPublic(), identity.getPublic()));
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                assertTrue("Mismatched failed identity", KeyUtils.compareKeys(badIdentity.getPublic(), identity.getPublic()));
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.addPublicKeyIdentity(badIdentity);
+                session.addPublicKeyIdentity(goodIdentity);
+                session.setPublicKeyAuthenticationReporter(reporter);
+                session.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+
+        List<PublicKey> expected = Arrays.asList(badIdentity.getPublic(), goodIdentity.getPublic());
+        // The server public key authenticator is called twice with the good identity
+        int numAttempted = attempted.size();
+        assertKeyListEquals("Attempted", expected, (numAttempted > 0) ? attempted.subList(0, numAttempted - 1) : attempted);
+        assertKeyListEquals("Reported", expected, reported);
+        // The signing is attempted only if the initial public key is accepted
+        assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed);
+    }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java b/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java
new file mode 100644
index 0000000..652092b
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java
@@ -0,0 +1,39 @@
+/*
+ * 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.util.test.server;
+
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.server.ServerFactoryManager;
+import org.apache.sshd.server.session.ServerSessionImpl;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class TestServerSession extends ServerSessionImpl {
+    public TestServerSession(ServerFactoryManager server, IoSession ioSession) throws Exception {
+        super(server, ioSession);
+    }
+
+    @Override
+    public void handleMessage(Buffer buffer) throws Exception {
+        super.handleMessage(buffer); // debug breakpoint
+    }
+}


[mina-sshd] 06/15: Ugraded PMD plugin version to 3.14.0

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 410bfbb37ee4dd036c25f15cc271884cf10d6122
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:54:29 2020 +0200

    Ugraded PMD plugin version to 3.14.0
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 4104e8d..d88edfb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1009,7 +1009,7 @@
                 <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-pmd-plugin</artifactId>
-                   <version>3.13.0</version>
+                   <version>3.14.0</version>
                    <configuration>
                        <targetJdk>${javac.target}</targetJdk>
                        <printFailingErrors>true</printFailingErrors>


[mina-sshd] 02/15: Ugraded Bouncycastle version to 1.68

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 8c0b7269235022672fd449fc6a7eaebc07dad261
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:52:03 2020 +0200

    Ugraded Bouncycastle version to 1.68
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 6b6d662..23853b5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,7 +104,7 @@
 
         <min.required.maven.version>3.5.0</min.required.maven.version>
         <groovy.version>3.0.7</groovy.version>
-        <bouncycastle.version>1.67</bouncycastle.version>
+        <bouncycastle.version>1.68</bouncycastle.version>
         <slf4j.version>1.7.30</slf4j.version>
         <logback.version>1.2.3</logback.version>        
         <spring.version>5.3.2</spring.version>


[mina-sshd] 08/15: Ugraded Netty version to 4.1.56.Final

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 71f3145e7d57548ff35d63697df535f97c9454e1
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:57:40 2020 +0200

    Ugraded Netty version to 4.1.56.Final
---
 sshd-netty/pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sshd-netty/pom.xml b/sshd-netty/pom.xml
index 13fa68a..4e6933f 100644
--- a/sshd-netty/pom.xml
+++ b/sshd-netty/pom.xml
@@ -40,7 +40,7 @@
             <dependency>
                 <groupId>io.netty</groupId>
                 <artifactId>netty-bom</artifactId>
-                <version>4.1.55.Final</version>
+                <version>4.1.56.Final</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>


[mina-sshd] 11/15: [SSHD-1114] Added callbacks for client-side host-based authentication progress

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit a8ee3aa1e3fdcf7014073e2ac4c1bf4a8eea4b8c
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 1 01:08:05 2021 +0200

    [SSHD-1114] Added callbacks for client-side host-based authentication progress
---
 CHANGES.md                                         |  1 +
 docs/event-listeners.md                            |  6 ++
 .../sshd/client/ClientAuthenticationManager.java   |  5 ++
 .../java/org/apache/sshd/client/SshClient.java     | 12 ++++
 .../hostbased/HostBasedAuthenticationReporter.java | 81 ++++++++++++++++++++++
 .../client/auth/hostbased/UserAuthHostBased.java   | 43 ++++++++++--
 .../sshd/client/session/AbstractClientSession.java | 14 ++++
 .../client/ClientAuthenticationManagerTest.java    | 11 +++
 .../sshd/common/auth/AuthenticationTest.java       | 76 +++++++++++++++++---
 9 files changed, 235 insertions(+), 14 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index c3aebd2..6a084dc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -28,3 +28,4 @@
 * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
+* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side host-based authentication progress
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 50869ff..1f17a98 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -205,3 +205,9 @@ overriding any globally registered instance.
 Used to inform about the progress of the client-side public key authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7).
 Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus
 overriding any globally registered instance.
+
+### `HostBasedAuthenticationReporter`
+
+Used to inform about the progress of the client-side host-based authentication as described in [RFC-4252 section 9](https://tools.ietf.org/html/rfc4252#section-9).
+Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus
+overriding any globally registered instance.
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
index b6c6706..4dff328 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
@@ -27,6 +27,7 @@ import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.BuiltinUserAuthFactories;
 import org.apache.sshd.client.auth.UserAuth;
 import org.apache.sshd.client.auth.UserAuthFactory;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
@@ -116,6 +117,10 @@ public interface ClientAuthenticationManager
 
     void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter);
 
+    HostBasedAuthenticationReporter getHostBasedAuthenticationReporter();
+
+    void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter);
+
     @Override
     default void setUserAuthFactoriesNames(Collection<String> names) {
         BuiltinUserAuthFactories.ParseResult result = BuiltinUserAuthFactories.parseFactoriesList(names);
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 2d07cfa..9660510 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -44,6 +44,7 @@ import java.util.stream.Collectors;
 import org.apache.sshd.agent.SshAgentFactory;
 import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.UserAuthFactory;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
 import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
@@ -183,6 +184,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
+    private HostBasedAuthenticationReporter hostBasedAuthenticationReporter;
     private UserInteraction userInteraction;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
@@ -272,6 +274,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
+    public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() {
+        return hostBasedAuthenticationReporter;
+    }
+
+    @Override
+    public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) {
+        this.hostBasedAuthenticationReporter = reporter;
+    }
+
+    @Override
     public List<UserAuthFactory> getUserAuthFactories() {
         return userAuthFactories;
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
new file mode 100644
index 0000000..18c2193
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.client.auth.hostbased;
+
+import java.security.KeyPair;
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * Provides report about the client side host-based authentication progress
+ *
+ * @see    <a href="https://tools.ietf.org/html/rfc4252#section-9">RFC-4252 section 9</a>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface HostBasedAuthenticationReporter {
+    /**
+     * Sending the initial request to use host based authentication
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted
+     * @param  hostname  The host name value sent to the server
+     * @param  username  The username value sent to the server
+     * @param  signature The signature data that is being sent to the server
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationAttempt(
+            ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted
+     * @param  hostname  The host name value sent to the server
+     * @param  username  The username value sent to the server
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationSuccess(
+            ClientSession session, String service, KeyPair identity, String hostname, String username)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session       The {@link ClientSession}
+     * @param  service       The requesting service name
+     * @param  identity      The {@link KeyPair} identity being attempted
+     * @param  hostname      The host name value sent to the server
+     * @param  username      The username value sent to the server
+     * @param  partial       {@code true} if some partial authentication success so far
+     * @param  serverMethods The {@link List} of authentication methods that can continue
+     * @throws Exception     If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationFailure(
+            ClientSession session, String service, KeyPair identity,
+            String hostname, String username, boolean partial, List<String> serverMethods)
+            throws Exception {
+        // ignored
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
index 861549a..5c18b5f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
@@ -48,8 +48,9 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
 public class UserAuthHostBased extends AbstractUserAuth implements SignatureFactoriesManager {
     public static final String NAME = UserAuthHostBasedFactory.NAME;
 
-    private Iterator<? extends Map.Entry<KeyPair, ? extends Collection<X509Certificate>>> keys;
-    private final HostKeyIdentityProvider clientHostKeys;
+    protected Iterator<? extends Map.Entry<KeyPair, ? extends Collection<X509Certificate>>> keys;
+    protected Map.Entry<KeyPair, ? extends Collection<X509Certificate>> keyInfo;
+    protected final HostKeyIdentityProvider clientHostKeys;
     private List<NamedFactory<Signature>> factories;
     private String clientUsername;
     private String clientHostname;
@@ -103,7 +104,7 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
             return false;
         }
 
-        Map.Entry<KeyPair, ? extends Collection<X509Certificate>> keyInfo = keys.next();
+        keyInfo = keys.next();
         KeyPair kp = keyInfo.getKey();
         PublicKey pub = kp.getPublic();
         String keyType = KeyUtils.getKeyType(pub);
@@ -159,13 +160,22 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
         buffer.putBytes(keyBytes);
         buffer.putString(clientHostname);
         buffer.putString(clientUsername);
-        appendSignature(session, service, keyType, pub, keyBytes, clientHostname, clientUsername, verifier, buffer);
+
+        byte[] signature = appendSignature(
+                session, service, keyType, pub, keyBytes,
+                clientHostname, clientUsername, verifier, buffer);
+        HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationAttempt(
+                    session, service, kp, clientHostname, clientUsername, signature);
+        }
+
         session.writePacket(buffer);
         return true;
     }
 
     @SuppressWarnings("checkstyle:ParameterNumber")
-    protected void appendSignature(
+    protected byte[] appendSignature(
             ClientSession session, String service,
             String keyType, PublicKey key, byte[] keyBytes,
             String clientHostname, String clientUsername,
@@ -203,6 +213,7 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
         bs.putString(keyType);
         bs.putBytes(signature);
         buffer.putBytes(bs.array(), bs.rpos(), bs.available());
+        return signature;
     }
 
     @Override
@@ -215,6 +226,28 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
                                         + " received unknown packet: cmd=" + SshConstants.getCommandMessageName(cmd));
     }
 
+    @Override
+    public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception {
+        HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationSuccess(
+                    session, service, (keyInfo == null) ? null : keyInfo.getKey(), resolveClientHostname(),
+                    resolveClientUsername());
+        }
+    }
+
+    @Override
+    public void signalAuthMethodFailure(
+            ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer)
+            throws Exception {
+        HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationFailure(
+                    session, service, (keyInfo == null) ? null : keyInfo.getKey(),
+                    resolveClientHostname(), resolveClientUsername(), partial, serverMethods);
+        }
+    }
+
     protected String resolveClientUsername() {
         String value = getClientUsername();
         return GenericUtils.isEmpty(value) ? OsUtils.getCurrentUser() : value;
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 490bbfb..6b1faff 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -32,6 +32,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.UserAuthFactory;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
@@ -95,6 +96,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
     private KeyIdentityProvider keyIdentityProvider;
     private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
+    private HostBasedAuthenticationReporter hostBasedAuthenticationReporter;
     private List<UserAuthFactory> userAuthFactories;
     private SocketAddress connectAddress;
     private ClientProxyConnector proxyConnector;
@@ -229,6 +231,18 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() {
+        ClientFactoryManager manager = getFactoryManager();
+        return resolveEffectiveProvider(HostBasedAuthenticationReporter.class, hostBasedAuthenticationReporter,
+                manager.getHostBasedAuthenticationReporter());
+    }
+
+    @Override
+    public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) {
+        this.hostBasedAuthenticationReporter = reporter;
+    }
+
+    @Override
     public ClientProxyConnector getClientProxyConnector() {
         ClientFactoryManager manager = getFactoryManager();
         return resolveEffectiveProvider(ClientProxyConnector.class, proxyConnector, manager.getClientProxyConnector());
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
index c1062e6..981f811 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
@@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider;
 import org.apache.sshd.client.auth.BuiltinUserAuthFactories;
 import org.apache.sshd.client.auth.UserAuthFactory;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
@@ -102,6 +103,16 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport {
             }
 
             @Override
+            public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() {
+                return null;
+            }
+
+            @Override
+            public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) {
+                throw new UnsupportedOperationException("setHostBasedAuthenticationReporter(" + reporter + ")");
+            }
+
+            @Override
             public UserInteraction getUserInteraction() {
                 return null;
             }
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
index c75d07d..beb3141 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
@@ -36,8 +36,10 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
 
 import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter;
 import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
@@ -72,6 +74,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.core.CoreModuleProperties;
 import org.apache.sshd.server.ServerFactoryManager;
 import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;
 import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
 import org.apache.sshd.server.auth.keyboard.InteractiveChallenge;
 import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
@@ -773,17 +776,72 @@ public class AuthenticationTest extends BaseTestSupport {
 
     @Test // see SSHD-620
     public void testHostBasedAuthentication() throws Exception {
-        String hostClienUser = getClass().getSimpleName();
+        AtomicInteger invocationCount = new AtomicInteger(0);
+        testHostBasedAuthentication(
+                (
+                        session, username, clientHostKey, clientHostName, clientUsername,
+                        certificates) -> invocationCount.incrementAndGet() > 0,
+                session -> {
+                    /* ignored */ });
+        assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get());
+    }
+
+    @Test   // see SSHD-1114
+    public void testHostBasedAuthenticationReporter() throws Exception {
+        AtomicReference<String> hostnameClientHolder = new AtomicReference<>();
+        AtomicReference<String> usernameClientHolder = new AtomicReference<>();
+        AtomicReference<PublicKey> keyClientHolder = new AtomicReference<>();
+        HostBasedAuthenticator authenticator
+                = (session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
+                    return Objects.equals(clientHostName, hostnameClientHolder.get())
+                            && Objects.equals(clientUsername, usernameClientHolder.get())
+                            && KeyUtils.compareKeys(clientHostKey, keyClientHolder.get());
+                };
+
+        HostBasedAuthenticationReporter reporter = new HostBasedAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature)
+                    throws Exception {
+                hostnameClientHolder.set(hostname);
+                usernameClientHolder.set(username);
+                keyClientHolder.set(identity.getPublic());
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(
+                    ClientSession session, String service, KeyPair identity, String hostname, String username)
+                    throws Exception {
+                assertEquals("Host", hostname, hostnameClientHolder.get());
+                assertEquals("User", username, usernameClientHolder.get());
+                assertKeyEquals("Identity", identity.getPublic(), keyClientHolder.get());
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, KeyPair identity,
+                    String hostname, String username, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                fail("Unexpected failure signalled");
+            }
+        };
+
+        testHostBasedAuthentication(authenticator, session -> session.setHostBasedAuthenticationReporter(reporter));
+    }
+
+    private void testHostBasedAuthentication(
+            HostBasedAuthenticator delegate, Consumer<? super ClientSession> preAuthInitializer)
+            throws Exception {
+        String hostClientUser = getClass().getSimpleName();
         String hostClientName = SshdSocketAddress.toAddressString(SshdSocketAddress.getFirstExternalNetwork4Address());
         KeyPair hostClientKey = CommonTestSupportUtils.generateKeyPair(
                 CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM,
                 CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE);
-        AtomicInteger invocationCount = new AtomicInteger(0);
         sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> {
-            invocationCount.incrementAndGet();
-            return hostClienUser.equals(clientUsername)
+            return hostClientUser.equals(clientUsername)
                     && hostClientName.equals(clientHostName)
-                    && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey);
+                    && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey)
+                    && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates);
         });
         sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
         sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
@@ -796,16 +854,16 @@ public class AuthenticationTest extends BaseTestSupport {
             org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory
                     = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory();
             // TODO factory.setClientHostname(CLIENT_HOSTNAME);
-            factory.setClientUsername(hostClienUser);
+            factory.setClientUsername(hostClientUser);
             factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey));
 
             client.setUserAuthFactories(Collections.singletonList(factory));
             client.start();
-            try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
                     .verify(CONNECT_TIMEOUT)
                     .getSession()) {
-                s.auth().verify(AUTH_TIMEOUT);
-                assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get());
+                preAuthInitializer.accept(session);
+                session.auth().verify(AUTH_TIMEOUT);
             } finally {
                 client.stop();
             }


[mina-sshd] 15/15: [SSHD-1114] Added HostBasedAuthenticationReporter#signalAuthenticationExhausted

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit f4aa59fd93aa8a17a7853cc397607aec081e73b9
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 1 08:38:10 2021 +0200

    [SSHD-1114] Added HostBasedAuthenticationReporter#signalAuthenticationExhausted
---
 docs/client-setup.md                               |  6 +++---
 .../hostbased/HostBasedAuthenticationReporter.java | 18 ++++++++++++++++
 .../client/auth/hostbased/UserAuthHostBased.java   | 25 +++++++++++++---------
 .../sshd/client/auth/pubkey/UserAuthPublicKey.java |  3 +--
 4 files changed, 37 insertions(+), 15 deletions(-)

diff --git a/docs/client-setup.md b/docs/client-setup.md
index 7f71922..0fc059e 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -100,9 +100,9 @@ This interface is required for full support of `keyboard-interactive` authentica
 The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be
 provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
 
-While RFC-4256 support is the primary purpose of this interface, it can also be used to retrieve the server's welcome banner as described
-in [RFC 4252 section 5.4](https://tools.ietf.org/html/rfc4252#section-5.4) as well as its initial identification string as described
-in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
+While ]RFC-4256](https://tools.ietf.org/html/rfc4256) support is the primary purpose of this interface, it can also be used to retrieve the server's
+welcome banner as described in [RFC 4252 section 5.4](https://tools.ietf.org/html/rfc4252#section-5.4) as well as its initial identification string
+as described in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
 
 In this context, regardless of whether such interaction is configured, the default implementation for the client side contains code
 that attempts to auto-detect a password prompt. If it detects it, then it attempts to use one of the registered passwords (if any) as
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
index 18c2193..749b5bc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java
@@ -49,6 +49,24 @@ public interface HostBasedAuthenticationReporter {
     }
 
     /**
+     * Signals end of host based attempts and optionally switching to other authentication methods. <B>Note:</B> neither
+     * {@link #signalAuthenticationSuccess(ClientSession, String, KeyPair, String, String) signalAuthenticationSuccess}
+     * nor {@link #signalAuthenticationFailure(ClientSession, String, KeyPair, String, String, boolean, List)
+     * signalAuthenticationFailure} are invoked.
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  hostname  The host name value sent to the server
+     * @param  username  The username value sent to the server
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationExhausted(
+            ClientSession session, String service, String hostname, String username)
+            throws Exception {
+        // ignored
+    }
+
+    /**
      * @param  session   The {@link ClientSession}
      * @param  service   The requesting service name
      * @param  identity  The {@link KeyPair} identity being attempted
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
index 5c18b5f..2e9ded2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
@@ -96,15 +96,22 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
     protected boolean sendAuthDataRequest(ClientSession session, String service) throws Exception {
         String name = getName();
         boolean debugEnabled = log.isDebugEnabled();
-        if ((keys == null) || (!keys.hasNext())) {
+        String clientUsername = resolveClientUsername(session);
+        String clientHostname = resolveClientHostname(session);
+        HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
+        keyInfo = ((keys != null) && keys.hasNext()) ? keys.next() : null;
+        if (keyInfo == null) {
             if (debugEnabled) {
                 log.debug("sendAuthDataRequest({})[{}][{}] no more keys to send", session, service, name);
             }
 
+            if (reporter != null) {
+                reporter.signalAuthenticationExhausted(session, service, clientUsername, clientHostname);
+            }
+
             return false;
         }
 
-        keyInfo = keys.next();
         KeyPair kp = keyInfo.getKey();
         PublicKey pub = kp.getPublic();
         String keyType = KeyUtils.getKeyType(pub);
@@ -124,8 +131,6 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
 
         byte[] id = session.getSessionId();
         String username = session.getUsername();
-        String clientUsername = resolveClientUsername();
-        String clientHostname = resolveClientHostname();
         if (debugEnabled) {
             log.debug("sendAuthDataRequest({})[{}][{}] client={}@{}",
                     session, service, name, clientUsername, clientHostname);
@@ -164,7 +169,6 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
         byte[] signature = appendSignature(
                 session, service, keyType, pub, keyBytes,
                 clientHostname, clientUsername, verifier, buffer);
-        HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
         if (reporter != null) {
             reporter.signalAuthenticationAttempt(
                     session, service, kp, clientHostname, clientUsername, signature);
@@ -231,8 +235,8 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
         HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter();
         if (reporter != null) {
             reporter.signalAuthenticationSuccess(
-                    session, service, (keyInfo == null) ? null : keyInfo.getKey(), resolveClientHostname(),
-                    resolveClientUsername());
+                    session, service, (keyInfo == null) ? null : keyInfo.getKey(),
+                    resolveClientHostname(session), resolveClientUsername(session));
         }
     }
 
@@ -244,16 +248,17 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact
         if (reporter != null) {
             reporter.signalAuthenticationFailure(
                     session, service, (keyInfo == null) ? null : keyInfo.getKey(),
-                    resolveClientHostname(), resolveClientUsername(), partial, serverMethods);
+                    resolveClientHostname(session), resolveClientUsername(session),
+                    partial, serverMethods);
         }
     }
 
-    protected String resolveClientUsername() {
+    protected String resolveClientUsername(ClientSession session) {
         String value = getClientUsername();
         return GenericUtils.isEmpty(value) ? OsUtils.getCurrentUser() : value;
     }
 
-    protected String resolveClientHostname() {
+    protected String resolveClientHostname(ClientSession session) {
         String value = getClientHostname();
         if (GenericUtils.isEmpty(value)) {
             value = SshdSocketAddress.toAddressString(
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
index 26ee56d..f55fc4e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
@@ -101,12 +101,12 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
             throw new RuntimeSshException(e);
         }
 
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
         if (current == null) {
             if (debugEnabled) {
                 log.debug("resolveAttemptedPublicKeyIdentity({})[{}] no more keys to send", session, service);
             }
 
-            PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
             if (reporter != null) {
                 reporter.signalAuthenticationExhausted(session, service);
             }
@@ -145,7 +145,6 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
                     session, service, name, algo, KeyUtils.getFingerPrint(pubKey));
         }
 
-        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
         if (reporter != null) {
             reporter.signalAuthenticationAttempt(session, service, keyPair, algo);
         }


[mina-sshd] 05/15: Ugraded PMD version to 6.30.0

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 62b1264c1ab5e846c53045d073e7d0987d001aab
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:53:46 2020 +0200

    Ugraded PMD version to 6.30.0
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 0885742..4104e8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,7 +117,7 @@
         <scm.plugin.version>1.11.2</scm.plugin.version>
         <plexus.archiver.version>4.2.3</plexus.archiver.version>
         <!-- See https://pmd.github.io/ for available latest version -->
-        <pmd.version>6.29.0</pmd.version>
+        <pmd.version>6.30.0</pmd.version>
 
         <sshd.tests.timeout.factor>1.0</sshd.tests.timeout.factor>
         <sshd.tests.rerun.count>2</sshd.tests.rerun.count>


[mina-sshd] 01/15: Ugraded ByteBuddy version to 1.10.19

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit b27c0a14bd1fc799cea33fb24340a8e4d2f8ae4d
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 20:51:20 2020 +0200

    Ugraded ByteBuddy version to 1.10.19
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index c4e8df0..6b6d662 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,7 +110,7 @@
         <spring.version>5.3.2</spring.version>
         <jgit.version>5.9.0.202009080501-r</jgit.version>
         <junit.version>4.13.1</junit.version>
-        <bytebuddy.version>1.10.18</bytebuddy.version>
+        <bytebuddy.version>1.10.19</bytebuddy.version>
 
         <surefire.plugin.version>3.0.0-M5</surefire.plugin.version>
         <maven.archiver.version>3.5.0</maven.archiver.version>


[mina-sshd] 10/15: [SSHD-1114] Added callbacks for client-side public key authentication progress

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit a94f45622ace7cad2d6dd78ef8980ff7e87858a3
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 23:18:04 2020 +0200

    [SSHD-1114] Added callbacks for client-side public key authentication progress
---
 CHANGES.md                                         |  1 +
 docs/event-listeners.md                            |  6 ++
 .../sshd/client/auth/pubkey/PublicKeyIdentity.java |  6 +-
 .../apache/sshd/util/test/JUnitTestSupport.java    | 13 +++
 .../main/java/org/apache/sshd/agent/SshAgent.java  | 10 +++
 .../sshd/agent/common/AbstractAgentProxy.java      |  1 +
 .../org/apache/sshd/agent/local/AgentImpl.java     |  6 ++
 .../sshd/client/ClientAuthenticationManager.java   |  5 ++
 .../java/org/apache/sshd/client/SshClient.java     | 14 +++-
 .../sshd/client/auth/pubkey/KeyAgentIdentity.java  | 20 +++--
 .../sshd/client/auth/pubkey/KeyPairIdentity.java   | 12 +--
 .../pubkey/PublicKeyAuthenticationReporter.java    | 92 ++++++++++++++++++++++
 .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 63 +++++++++++----
 .../sshd/client/session/AbstractClientSession.java | 14 ++++
 .../client/ClientAuthenticationManagerTest.java    | 11 +++
 .../sshd/common/auth/AuthenticationTest.java       | 85 +++++++++++++++++++-
 16 files changed, 326 insertions(+), 33 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 75ae5f7..c3aebd2 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,3 +27,4 @@
 * [SSHD-1085](https://issues.apache.org/jira/browse/SSHD-1085) Added more notifications related to channel state change for detecting channel closing or closed earlier.
 * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
+* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 3f9dcf6..50869ff 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -199,3 +199,9 @@ in [RFC 4254 - section 6.7](https://tools.ietf.org/html/rfc4254#section-6.7)
 Used to inform about the progress of the client-side password based authentication as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
 Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus
 overriding any globally registered instance.
+
+### `PublicKeyAuthenticationReporter`
+
+Used to inform about the progress of the client-side public key authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7).
+Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus
+overriding any globally registered instance.
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java b/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
index e794986..f0f3634 100644
--- a/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
+++ b/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
@@ -18,7 +18,7 @@
  */
 package org.apache.sshd.client.auth.pubkey;
 
-import java.security.PublicKey;
+import java.security.KeyPair;
 import java.util.Map;
 
 import org.apache.sshd.common.session.SessionContext;
@@ -30,9 +30,9 @@ import org.apache.sshd.common.session.SessionContext;
  */
 public interface PublicKeyIdentity {
     /**
-     * @return The {@link PublicKey} identity value
+     * @return The {@link KeyPair} identity value
      */
-    PublicKey getPublicKey();
+    KeyPair getKeyIdentity();
 
     /**
      * Proves the public key identity by signing the given data
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index 98d870f..ff3a329 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -455,6 +455,19 @@ public abstract class JUnitTestSupport extends Assert {
         assertArrayEquals(message + "[encoded-data]", expected.getEncoded(), actual.getEncoded());
     }
 
+    public static <T extends Key> void assertKeyListEquals(
+            String message, List<? extends T> expected, List<? extends T> actual) {
+        int numKeys = GenericUtils.size(expected);
+        assertEquals(message + "[size]", numKeys, GenericUtils.size(actual));
+        if (numKeys <= 0) {
+            return;
+        }
+
+        for (int index = 0; index < numKeys; index++) {
+            assertKeyEquals(message + "[#" + index + "]", expected.get(index), actual.get(index));
+        }
+    }
+
     public static <T extends Key> void assertKeyEquals(String message, T expected, T actual) {
         if (expected == actual) {
             return;
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java b/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
index 1c25f3a..700c69c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
@@ -47,6 +47,16 @@ public interface SshAgent extends java.nio.channels.Channel {
      */
     Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key, String algo, byte[] data) throws IOException;
 
+    /**
+     * Used for reporting client-side public key authentication via agent
+     *
+     * @param  key The {@link PublicKey} that is going to be used
+     * @return     The {@link KeyPair} identity for it - if available - {@code null} otherwise
+     */
+    default KeyPair resolveLocalIdentity(PublicKey key) {
+        return null;
+    }
+
     void addIdentity(KeyPair key, String comment) throws IOException;
 
     void removeIdentity(PublicKey key) throws IOException;
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
index 3add243..da38c72 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
@@ -83,6 +83,7 @@ public abstract class AbstractAgentProxy extends AbstractLoggingBean implements
         }
 
         int nbIdentities = buffer.getInt();
+        // TODO make the maximum a Property
         if ((nbIdentities < 0) || (nbIdentities > 1024)) {
             throw new SshException("Illogical identities count: " + nbIdentities);
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java b/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
index 2b80370..bb50ce6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
@@ -105,6 +105,12 @@ public class AgentImpl implements SshAgent {
     }
 
     @Override
+    public KeyPair resolveLocalIdentity(PublicKey key) {
+        Map.Entry<KeyPair, String> pp = getKeyPair(keys, key);
+        return (pp == null) ? null : pp.getKey();
+    }
+
+    @Override
     public void removeIdentity(PublicKey key) throws IOException {
         if (!isOpen()) {
             throw new SshException("Agent closed");
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
index 14bca04..b6c6706 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java
@@ -30,6 +30,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.auth.UserAuthFactoriesManager;
@@ -111,6 +112,10 @@ public interface ClientAuthenticationManager
 
     void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter);
 
+    PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter();
+
+    void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter);
+
     @Override
     default void setUserAuthFactoriesNames(Collection<String> names) {
         BuiltinUserAuthFactories.ParseResult result = BuiltinUserAuthFactories.parseFactoriesList(names);
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 4182bf8..2d07cfa 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -49,6 +49,7 @@ import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
 import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
@@ -178,10 +179,11 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private HostConfigEntryResolver hostConfigEntryResolver;
     private ClientIdentityLoader clientIdentityLoader;
     private KeyIdentityProvider keyIdentityProvider;
+    private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
-    private UserInteraction userInteraction;
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
+    private UserInteraction userInteraction;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -359,6 +361,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
+    public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+        return publicKeyAuthenticationReporter;
+    }
+
+    @Override
+    public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+        this.publicKeyAuthenticationReporter = reporter;
+    }
+
+    @Override
     protected void checkConfig() {
         super.checkConfig();
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
index 53b37b2..2cec320 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.client.auth.pubkey;
 
+import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Map;
 import java.util.Objects;
@@ -33,18 +34,23 @@ import org.apache.sshd.common.session.SessionContext;
  */
 public class KeyAgentIdentity implements PublicKeyIdentity {
     private final SshAgent agent;
-    private final PublicKey key;
+    private final KeyPair keyPair;
+    private KeyPair resolvedPair;
     private final String comment;
 
     public KeyAgentIdentity(SshAgent agent, PublicKey key, String comment) {
         this.agent = Objects.requireNonNull(agent, "No signing agent");
-        this.key = Objects.requireNonNull(key, "No public key");
+        this.keyPair = new KeyPair(Objects.requireNonNull(key, "No public key"), null);
         this.comment = comment;
     }
 
     @Override
-    public PublicKey getPublicKey() {
-        return key;
+    public KeyPair getKeyIdentity() {
+        if (resolvedPair == null) {
+            resolvedPair = agent.resolveLocalIdentity(keyPair.getPublic());
+        }
+
+        return (resolvedPair == null) ? keyPair : resolvedPair;
     }
 
     public String getComment() {
@@ -53,12 +59,14 @@ public class KeyAgentIdentity implements PublicKeyIdentity {
 
     @Override
     public Map.Entry<String, byte[]> sign(SessionContext session, String algo, byte[] data) throws Exception {
-        return agent.sign(session, getPublicKey(), algo, data);
+        KeyPair kp = getKeyIdentity();
+        return agent.sign(session, kp.getPublic(), algo, data);
     }
 
     @Override
     public String toString() {
-        PublicKey pubKey = getPublicKey();
+        KeyPair kp = getKeyIdentity();
+        PublicKey pubKey = kp.getPublic();
         return getClass().getSimpleName() + "[" + KeyUtils.getKeyType(pubKey) + "]"
                + " fingerprint=" + KeyUtils.getFingerPrint(pubKey)
                + ", comment=" + getComment();
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
index b90c369..6dfcf72 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
@@ -43,7 +43,7 @@ import org.apache.sshd.common.util.ValidateUtils;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHolder {
-    protected final KeyPair pair;
+    private final KeyPair pair;
     private final List<NamedFactory<Signature>> signatureFactories;
 
     public KeyPairIdentity(SignatureFactoriesManager primary, SignatureFactoriesManager secondary, KeyPair pair) {
@@ -55,8 +55,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
     }
 
     @Override
-    public PublicKey getPublicKey() {
-        return pair.getPublic();
+    public KeyPair getKeyIdentity() {
+        return pair;
     }
 
     @Override
@@ -68,7 +68,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
     public Map.Entry<String, byte[]> sign(SessionContext session, String algo, byte[] data) throws Exception {
         NamedFactory<? extends Signature> factory;
         if (GenericUtils.isEmpty(algo)) {
-            algo = KeyUtils.getKeyType(getPublicKey());
+            KeyPair kp = getKeyIdentity();
+            algo = KeyUtils.getKeyType(kp.getPublic());
             // SSHD-1104 check if the key type is aliased
             factory = SignatureFactory.resolveSignatureFactory(algo, getSignatureFactories());
         } else {
@@ -86,7 +87,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
 
     @Override
     public String toString() {
-        PublicKey pubKey = getPublicKey();
+        KeyPair kp = getKeyIdentity();
+        PublicKey pubKey = kp.getPublic();
         return getClass().getSimpleName()
                + " type=" + KeyUtils.getKeyType(pubKey)
                + ", factories=" + getSignatureFactoriesNameList()
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
new file mode 100644
index 0000000..b5900b8
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
@@ -0,0 +1,92 @@
+/*
+ * 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.client.auth.pubkey;
+
+import java.security.KeyPair;
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * Provides report about the client side public key authentication progress
+ *
+ * @see    <a href="https://tools.ietf.org/html/rfc4252#section-7">RFC-4252 section 7</a>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PublicKeyAuthenticationReporter {
+    /**
+     * Sending the initial request to use public key authentication
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @param  signature The type of signature that is being used
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationAttempt(
+            ClientSession session, String service, KeyPair identity, String signature)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * Sending the signed response to the server's challenge
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @param  signature The type of signature that is being used
+     * @param  signed    The generated signature data
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalSignatureAttempt(
+            ClientSession session, String service, KeyPair identity, String signature, byte[] signed)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session       The {@link ClientSession}
+     * @param  service       The requesting service name
+     * @param  identity      The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications
+     *                       the private key may be {@code null}
+     * @param  partial       {@code true} if some partial authentication success so far
+     * @param  serverMethods The {@link List} of authentication methods that can continue
+     * @throws Exception     If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationFailure(
+            ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods)
+            throws Exception {
+        // ignored
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
index 28e575e..16fab44 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
@@ -20,6 +20,7 @@ package org.apache.sshd.client.auth.pubkey;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.security.KeyPair;
 import java.security.PublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.util.Collection;
@@ -111,16 +112,17 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
             log.trace("sendAuthDataRequest({})[{}] current key details: {}", session, service, current);
         }
 
-        PublicKey key;
+        KeyPair keyPair;
         try {
-            key = current.getPublicKey();
+            keyPair = current.getKeyIdentity();
         } catch (Error e) {
-            warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve public key: {}",
+            warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve key identity: {}",
                     session, service, e.getClass().getSimpleName(), e.getMessage(), e);
             throw new RuntimeSshException(e);
         }
 
-        String keyType = KeyUtils.getKeyType(key);
+        PublicKey pubKey = keyPair.getPublic();
+        String keyType = KeyUtils.getKeyType(pubKey);
         NamedFactory<? extends Signature> factory;
         // SSHD-1104 check if the key type is aliased
         if (current instanceof SignatureFactoriesHolder) {
@@ -134,7 +136,12 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         String name = getName();
         if (debugEnabled) {
             log.debug("sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST request {} type={} - fingerprint={}",
-                    session, service, name, algo, KeyUtils.getFingerPrint(key));
+                    session, service, name, algo, KeyUtils.getFingerPrint(pubKey));
+        }
+
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationAttempt(session, service, keyPair, algo);
         }
 
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
@@ -143,7 +150,7 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         buffer.putString(name);
         buffer.putBoolean(false);
         buffer.putString(algo);
-        buffer.putPublicKey(key);
+        buffer.putPublicKey(pubKey);
         session.writePacket(buffer);
         return true;
     }
@@ -161,17 +168,18 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         /*
          * Make sure the server echo-ed the same key we sent as sanctioned by RFC4252 section 7
          */
-        PublicKey key;
+        KeyPair keyPair;
         boolean debugEnabled = log.isDebugEnabled();
         try {
-            key = current.getPublicKey();
+            keyPair = current.getKeyIdentity();
         } catch (Error e) {
-            warn("processAuthDataRequest({})[{}][{}] failed ({}) to retrieve public key: {}",
+            warn("processAuthDataRequest({})[{}][{}] failed ({}) to retrieve key identity: {}",
                     session, service, name, e.getClass().getSimpleName(), e.getMessage(), e);
             throw new RuntimeSshException(e);
         }
 
-        String curKeyType = KeyUtils.getKeyType(key);
+        PublicKey pubKey = keyPair.getPublic();
+        String curKeyType = KeyUtils.getKeyType(pubKey);
         String rspKeyType = buffer.getString();
         Collection<String> aliases = KeyUtils.getAllEquivalentKeyTypes(curKeyType);
         String algo;
@@ -193,10 +201,10 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         }
 
         PublicKey rspKey = buffer.getPublicKey();
-        if (!KeyUtils.compareKeys(rspKey, key)) {
+        if (!KeyUtils.compareKeys(rspKey, pubKey)) {
             throw new InvalidKeySpecException(
                     "processAuthDataRequest(" + session + ")[" + service + "][" + name + "]"
-                                              + " mismatched " + algo + " keys: expected=" + KeyUtils.getFingerPrint(key)
+                                              + " mismatched " + algo + " keys: expected=" + KeyUtils.getFingerPrint(pubKey)
                                               + ", actual=" + KeyUtils.getFingerPrint(rspKey));
         }
 
@@ -216,14 +224,19 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         buffer.putString(name);
         buffer.putBoolean(true);
         buffer.putString(algo);
-        buffer.putPublicKey(key);
-        appendSignature(session, service, name, username, algo, key, buffer);
+        buffer.putPublicKey(pubKey);
+
+        byte[] sig = appendSignature(session, service, name, username, algo, pubKey, buffer);
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalSignatureAttempt(session, service, keyPair, algo, sig);
+        }
 
         session.writePacket(buffer);
         return true;
     }
 
-    protected void appendSignature(
+    protected byte[] appendSignature(
             ClientSession session, String service, String name, String username, String algo, PublicKey key, Buffer buffer)
             throws Exception {
         byte[] id = session.getSessionId();
@@ -265,6 +278,26 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         bs.putString(algo);
         bs.putBytes(sig);
         buffer.putBytes(bs.array(), bs.rpos(), bs.available());
+        return sig;
+    }
+
+    @Override
+    public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception {
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationSuccess(session, service, (current == null) ? null : current.getKeyIdentity());
+        }
+    }
+
+    @Override
+    public void signalAuthMethodFailure(
+            ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer)
+            throws Exception {
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            KeyPair identity = (current == null) ? null : current.getKeyIdentity();
+            reporter.signalAuthenticationFailure(session, service, identity, partial, serverMethods);
+        }
     }
 
     @Override
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 4c5eff4..490bbfb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -35,6 +35,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ChannelShell;
@@ -93,6 +94,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private PasswordIdentityProvider passwordIdentityProvider;
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
     private KeyIdentityProvider keyIdentityProvider;
+    private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
     private List<UserAuthFactory> userAuthFactories;
     private SocketAddress connectAddress;
     private ClientProxyConnector proxyConnector;
@@ -215,6 +217,18 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+        ClientFactoryManager manager = getFactoryManager();
+        return resolveEffectiveProvider(PublicKeyAuthenticationReporter.class, publicKeyAuthenticationReporter,
+                manager.getPublicKeyAuthenticationReporter());
+    }
+
+    @Override
+    public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+        this.publicKeyAuthenticationReporter = reporter;
+    }
+
+    @Override
     public ClientProxyConnector getClientProxyConnector() {
         ClientFactoryManager manager = getFactoryManager();
         return resolveEffectiveProvider(ClientProxyConnector.class, proxyConnector, manager.getClientProxyConnector());
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
index 40df790..c1062e6 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java
@@ -33,6 +33,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionImpl;
@@ -91,6 +92,16 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport {
             }
 
             @Override
+            public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+                return null;
+            }
+
+            @Override
+            public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+                throw new UnsupportedOperationException("setPublicKeyAuthenticationReporter(" + reporter + ")");
+            }
+
+            @Override
             public UserInteraction getUserInteraction() {
                 return null;
             }
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
index 2dbe763..c75d07d 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
@@ -42,6 +42,7 @@ import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
 import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.AttributeRepository;
@@ -996,13 +997,21 @@ public class AuthenticationTest extends BaseTestSupport {
     public void testPasswordAuthenticationReporter() throws Exception {
         String goodPassword = getCurrentTestName();
         String badPassword = getClass().getSimpleName();
-        List<String> actual = new ArrayList<>();
+        List<String> attempted = new ArrayList<>();
+        sshd.setPasswordAuthenticator((user, password, session) -> {
+            attempted.add(password);
+            return goodPassword.equals(password);
+        });
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+
+        List<String> reported = new ArrayList<>();
         PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() {
             @Override
             public void signalAuthenticationAttempt(
                     ClientSession session, String service, String oldPassword, boolean modified, String newPassword)
                     throws Exception {
-                actual.add(oldPassword);
+                reported.add(oldPassword);
             }
 
             @Override
@@ -1035,7 +1044,77 @@ public class AuthenticationTest extends BaseTestSupport {
             }
         }
 
-        assertListEquals("Attempted passwords", Arrays.asList(badPassword, goodPassword), actual);
+        List<String> expected = Arrays.asList(badPassword, goodPassword);
+        assertListEquals("Attempted passwords", expected, attempted);
+        assertListEquals("Reported passwords", expected, reported);
+    }
+
+    @Test   // see SSHD-1114
+    public void testPublicKeyAuthenticationReporter() throws Exception {
+        KeyPair goodIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        KeyPair badIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        List<PublicKey> attempted = new ArrayList<>();
+        sshd.setPublickeyAuthenticator((username, key, session) -> {
+            attempted.add(key);
+            return KeyUtils.compareKeys(goodIdentity.getPublic(), key);
+        });
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+
+        List<PublicKey> reported = new ArrayList<>();
+        List<PublicKey> signed = new ArrayList<>();
+        PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationAttempt(
+                    ClientSession session, String service, KeyPair identity, String signature)
+                    throws Exception {
+                reported.add(identity.getPublic());
+            }
+
+            @Override
+            public void signalSignatureAttempt(
+                    ClientSession session, String service, KeyPair identity, String signature, byte[] sigData)
+                    throws Exception {
+                signed.add(identity.getPublic());
+            }
+
+            @Override
+            public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity)
+                    throws Exception {
+                assertTrue("Mismatched success identity", KeyUtils.compareKeys(goodIdentity.getPublic(), identity.getPublic()));
+            }
+
+            @Override
+            public void signalAuthenticationFailure(
+                    ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods)
+                    throws Exception {
+                assertTrue("Mismatched failed identity", KeyUtils.compareKeys(badIdentity.getPublic(), identity.getPublic()));
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.addPublicKeyIdentity(badIdentity);
+                session.addPublicKeyIdentity(goodIdentity);
+                session.setPublicKeyAuthenticationReporter(reporter);
+                session.auth().verify(AUTH_TIMEOUT);
+            } finally {
+                client.stop();
+            }
+        }
+
+        List<PublicKey> expected = Arrays.asList(badIdentity.getPublic(), goodIdentity.getPublic());
+        // The server public key authenticator is called twice with the good identity
+        int numAttempted = attempted.size();
+        assertKeyListEquals("Attempted", expected, (numAttempted > 0) ? attempted.subList(0, numAttempted - 1) : attempted);
+        assertKeyListEquals("Reported", expected, reported);
+        // The signing is attempted only if the initial public key is accepted
+        assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed);
     }
 
     private static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException {


[mina-sshd] 13/15: [SSHD-1114] Added capability for interactive password authentication participation via UserInteraction

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 25a3823b36d9ca5f306e3cb63652224478a64252
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 1 07:32:47 2021 +0200

    [SSHD-1114] Added capability for interactive password authentication participation via UserInteraction
---
 CHANGES.md                                         |  1 +
 docs/client-setup.md                               | 20 +++++--
 .../sshd/client/auth/keyboard/UserInteraction.java | 11 ++++
 .../password/PasswordAuthenticationReporter.java   | 14 +++++
 .../client/auth/password/UserAuthPassword.java     | 24 +++++++--
 .../common/auth/PasswordAuthenticationTest.java    | 62 ++++++++++++++++++++++
 6 files changed, 126 insertions(+), 6 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 6a084dc..e0598d0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -29,3 +29,4 @@
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side host-based authentication progress
+* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added capability for interactive password authentication participation via UserInteraction
diff --git a/docs/client-setup.md b/docs/client-setup.md
index 0837186..55b64d8 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -96,12 +96,12 @@ our default limit seems quite suitable (and beyond) for most cases we are likely
 
 ### `UserInteraction`
 
-This interface is required for full support of `keyboard-interactive` authentication protocol as described in [RFC 4256](https://www.ietf.org/rfc/rfc4256.txt).
+This interface is required for full support of `keyboard-interactive` authentication protocol as described in [RFC-4252 section 9](https://tools.ietf.org/html/rfc4252#section-9).
 The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be
-provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://www.ietf.org/rfc/rfc4252.txt).
+provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
 
 While RFC-4256 support is the primary purpose of this interface, it can also be used to retrieve the server's welcome banner as described
-in [RFC 4252 section 5.4](https://www.ietf.org/rfc/rfc4252.txt) as well as its initial identification string as described
+in [RFC 4252 section 5.4](https://tools.ietf.org/html/rfc4252#section-5.4) as well as its initial identification string as described
 in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
 
 In this context, regardless of whether such interaction is configured, the default implementation for the client side contains code
@@ -110,6 +110,20 @@ the interactive response to the server's challenge - (see client-side implementa
 method). Basically, detection occurs by checking if the server sent **exactly one** challenge with no requested echo, and the challenge
 string looks like `"... password ...:"` (**Note:** the auto-detection and password prompt detection patterns are configurable).
 
+This interface can also be used to easily implement interactive password request from user for the `password` authentication protocol
+as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section-8) via the `resolveAuthPasswordAttempt` method.
+
+```java
+/**
+ * Invoked during password authentication when no more pre-registered passwords are available
+ *
+ * @param  session The {@link ClientSession} through which the request was received
+ * @return The password to use - {@code null} signals no more passwords available
+ * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
+ */
+String resolveAuthPasswordAttempt(ClientSession session) throws Exception;
+```
+
 ## Using the `SshClient` to connect to a server
 
 Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server.
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
index ff4e878..f5101f8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
@@ -149,6 +149,17 @@ public interface UserInteraction {
     String getUpdatedPassword(ClientSession session, String prompt, String lang);
 
     /**
+     * Invoked during password authentication when no more pre-registered passwords are available
+     *
+     * @param  session   The {@link ClientSession} through which the request was received
+     * @return           The password to use - {@code null} signals no more passwords available
+     * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
+     */
+    default String resolveAuthPasswordAttempt(ClientSession session) throws Exception {
+        return null;
+    }
+
+    /**
      * @param  prompt     The user interaction prompt
      * @param  tokensList A comma-separated list of tokens whose <U>last</U> index is prompt is sought.
      * @return            The position of any token in the prompt - negative if not found
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
index 3ebdb71..1a0643e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/PasswordAuthenticationReporter.java
@@ -45,6 +45,20 @@ public interface PasswordAuthenticationReporter {
     }
 
     /**
+     * Signals end of passwords attempts and optionally switching to other authentication methods. <B>Note:</B> neither
+     * {@link #signalAuthenticationSuccess(ClientSession, String, String) signalAuthenticationSuccess} nor
+     * {@link #signalAuthenticationFailure(ClientSession, String, String, boolean, List) signalAuthenticationFailure}
+     * are invoked.
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationExhausted(ClientSession session, String service) throws Exception {
+        // ignored
+    }
+
+    /**
      * @param  session   The {@link ClientSession}
      * @param  service   The requesting service name
      * @param  password  The password that was attempted
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
index e99dfe8..3490000 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java
@@ -62,15 +62,20 @@ public class UserAuthPassword extends AbstractUserAuth {
             return false;
         }
 
-        if ((passwords == null) || (!passwords.hasNext())) {
+        current = resolveAttemptedPassword(session, service);
+        if (current == null) {
             if (log.isDebugEnabled()) {
-                log.debug("sendAuthDataRequest({})[{}] no more passwords to send", session, service);
+                log.debug("resolveAttemptedPassword({})[{}] no more passwords to send", session, service);
+            }
+
+            PasswordAuthenticationReporter reporter = session.getPasswordAuthenticationReporter();
+            if (reporter != null) {
+                reporter.signalAuthenticationExhausted(session, service);
             }
 
             return false;
         }
 
-        current = passwords.next();
         String username = session.getUsername();
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST,
                 username.length() + service.length()
@@ -85,6 +90,19 @@ public class UserAuthPassword extends AbstractUserAuth {
         return true;
     }
 
+    protected String resolveAttemptedPassword(ClientSession session, String service) throws Exception {
+        if ((passwords != null) && passwords.hasNext()) {
+            return passwords.next();
+        }
+
+        UserInteraction ui = session.getUserInteraction();
+        if ((ui == null) || (!ui.isInteractionAllowed(session))) {
+            return null;
+        }
+
+        return ui.resolveAuthPasswordAttempt(session);
+    }
+
     @Override
     protected boolean processAuthDataRequest(
             ClientSession session, String service, Buffer buffer)
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
index 628338d..7c04231 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java
@@ -437,4 +437,66 @@ public class PasswordAuthenticationTest extends AuthenticationTestSupport {
         assertListEquals("Attempted passwords", expected, attempted);
         assertListEquals("Reported passwords", expected, reported);
     }
+
+    @Test   // see SSHD-1114
+    public void testAuthenticationAttemptsExhausted() throws Exception {
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+
+        AtomicInteger exhaustedCount = new AtomicInteger();
+        PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationExhausted(ClientSession session, String service) throws Exception {
+                exhaustedCount.incrementAndGet();
+            }
+        };
+
+        AtomicInteger attemptsCount = new AtomicInteger();
+        UserInteraction ui = new UserInteraction() {
+            @Override
+            public String[] interactive(
+                    ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+                throw new UnsupportedOperationException("Unexpected interactive invocation");
+            }
+
+            @Override
+            public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                throw new UnsupportedOperationException("Unexpected updated password request");
+            }
+
+            @Override
+            public String resolveAuthPasswordAttempt(ClientSession session) throws Exception {
+                int count = attemptsCount.incrementAndGet();
+                if (count <= 3) {
+                    return "attempt#" + count;
+                } else {
+                    return UserInteraction.super.resolveAuthPasswordAttempt(session);
+                }
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.setPasswordAuthenticationReporter(reporter);
+                session.setUserInteraction(ui);
+                for (int index = 1; index <= 5; index++) {
+                    session.addPasswordIdentity("password#" + index);
+                }
+
+                AuthFuture auth = session.auth();
+                assertAuthenticationResult("Authenticating", auth, false);
+            } finally {
+                client.stop();
+            }
+        }
+
+        assertEquals("Mismatched invocation count", 1, exhaustedCount.getAndSet(0));
+        assertEquals("Mismatched retries count", 4 /* 3 attempts + null */, attemptsCount.getAndSet(0));
+    }
 }


[mina-sshd] 14/15: [SSHD-1114] Added capability for interactive key based authentication participation via UserInteraction

Posted by lg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 106778423eb506584393631a27ada6e5a4ec36f1
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Jan 1 07:59:08 2021 +0200

    [SSHD-1114] Added capability for interactive key based authentication participation via UserInteraction
---
 CHANGES.md                                         |  1 +
 docs/client-setup.md                               | 14 +++++
 .../sshd/client/auth/keyboard/UserInteraction.java | 13 ++++-
 .../pubkey/PublicKeyAuthenticationReporter.java    | 14 +++++
 .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 42 +++++++++++---
 .../common/auth/PublicKeyAuthenticationTest.java   | 65 ++++++++++++++++++++++
 6 files changed, 139 insertions(+), 10 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index e0598d0..69e03b3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -30,3 +30,4 @@
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side host-based authentication progress
 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added capability for interactive password authentication participation via UserInteraction
+* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added capability for interactive key based authentication participation via UserInteraction
diff --git a/docs/client-setup.md b/docs/client-setup.md
index 55b64d8..7f71922 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -124,6 +124,20 @@ as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section
 String resolveAuthPasswordAttempt(ClientSession session) throws Exception;
 ```
 
+The interface can also be used to implement interactive key based authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7)
+via the `resolveAuthPublicKeyIdentityAttempt` method.
+
+```java
+/**
+ * Invoked during public key authentication when no more pre-registered keys are available
+ *
+ * @param  session   The {@link ClientSession} through which the request was received
+ * @return           The {@link KeyPair} to use - {@code null} signals no more keys available
+ * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
+ */
+KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception;
+```
+
 ## Using the `SshClient` to connect to a server
 
 Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server.
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
index f5101f8..53304c4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.client.auth.keyboard;
 
+import java.security.KeyPair;
 import java.util.List;
 
 import org.apache.sshd.client.session.ClientSession;
@@ -88,7 +89,6 @@ public interface UserInteraction {
     };
 
     /**
-     *
      * @param  session The {@link ClientSession}
      * @return         {@code true} if user interaction allowed for this session (default)
      */
@@ -160,6 +160,17 @@ public interface UserInteraction {
     }
 
     /**
+     * Invoked during public key authentication when no more pre-registered keys are available
+     *
+     * @param  session   The {@link ClientSession} through which the request was received
+     * @return           The {@link KeyPair} to use - {@code null} signals no more keys available
+     * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
+     */
+    default KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception {
+        return null;
+    }
+
+    /**
      * @param  prompt     The user interaction prompt
      * @param  tokensList A comma-separated list of tokens whose <U>last</U> index is prompt is sought.
      * @return            The position of any token in the prompt - negative if not found
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
index b5900b8..7719721 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
@@ -48,6 +48,20 @@ public interface PublicKeyAuthenticationReporter {
     }
 
     /**
+     * Signals end of public key attempts and optionally switching to other authentication methods. <B>Note:</B> neither
+     * {@link #signalAuthenticationSuccess(ClientSession, String, KeyPair) signalAuthenticationSuccess} nor
+     * {@link #signalAuthenticationFailure(ClientSession, String, KeyPair, boolean, List) signalAuthenticationFailure}
+     * are invoked.
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationExhausted(ClientSession session, String service) throws Exception {
+        // ignored
+    }
+
+    /**
      * Sending the signed response to the server's challenge
      *
      * @param  session   The {@link ClientSession}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
index 16fab44..26ee56d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.sshd.client.auth.AbstractUserAuth;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.RuntimeSshException;
@@ -93,21 +94,26 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
     protected boolean sendAuthDataRequest(ClientSession session, String service) throws Exception {
         boolean debugEnabled = log.isDebugEnabled();
         try {
-            if ((keys == null) || (!keys.hasNext())) {
-                if (debugEnabled) {
-                    log.debug("sendAuthDataRequest({})[{}] no more keys to send", session, service);
-                }
-
-                return false;
-            }
-
-            current = keys.next();
+            current = resolveAttemptedPublicKeyIdentity(session, service);
         } catch (Error e) {
             warn("sendAuthDataRequest({})[{}] failed ({}) to get next key: {}",
                     session, service, e.getClass().getSimpleName(), e.getMessage(), e);
             throw new RuntimeSshException(e);
         }
 
+        if (current == null) {
+            if (debugEnabled) {
+                log.debug("resolveAttemptedPublicKeyIdentity({})[{}] no more keys to send", session, service);
+            }
+
+            PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+            if (reporter != null) {
+                reporter.signalAuthenticationExhausted(session, service);
+            }
+
+            return false;
+        }
+
         if (log.isTraceEnabled()) {
             log.trace("sendAuthDataRequest({})[{}] current key details: {}", session, service, current);
         }
@@ -155,6 +161,24 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         return true;
     }
 
+    protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(ClientSession session, String service) throws Exception {
+        if ((keys != null) && keys.hasNext()) {
+            return keys.next();
+        }
+
+        UserInteraction ui = session.getUserInteraction();
+        if ((ui == null) || (!ui.isInteractionAllowed(session))) {
+            return null;
+        }
+
+        KeyPair kp = ui.resolveAuthPublicKeyIdentityAttempt(session);
+        if (kp == null) {
+            return null;
+        }
+
+        return new KeyPairIdentity(this, session, kp);
+    }
+
     @Override
     protected boolean processAuthDataRequest(ClientSession session, String service, Buffer buffer) throws Exception {
         String name = getName();
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
index 300b77e..3b86eb5 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java
@@ -33,7 +33,9 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
+import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
@@ -53,6 +55,7 @@ import org.apache.sshd.common.util.io.resource.URLResource;
 import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator;
 import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
 import org.apache.sshd.util.test.CommonTestSupportUtils;
 import org.apache.sshd.util.test.CoreTestSupportUtils;
@@ -324,4 +327,66 @@ public class PublicKeyAuthenticationTest extends AuthenticationTestSupport {
         // The signing is attempted only if the initial public key is accepted
         assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed);
     }
+
+    @Test   // see SSHD-1114
+    public void testAuthenticationAttemptsExhausted() throws Exception {
+        sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE);
+        sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE);
+        sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE);
+
+        AtomicInteger exhaustedCount = new AtomicInteger();
+        PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() {
+            @Override
+            public void signalAuthenticationExhausted(ClientSession session, String service) throws Exception {
+                exhaustedCount.incrementAndGet();
+            }
+        };
+
+        KeyPair kp = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256);
+        AtomicInteger attemptsCount = new AtomicInteger();
+        UserInteraction ui = new UserInteraction() {
+            @Override
+            public String[] interactive(
+                    ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+                throw new UnsupportedOperationException("Unexpected interactive invocation");
+            }
+
+            @Override
+            public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
+                throw new UnsupportedOperationException("Unexpected updated password request");
+            }
+
+            @Override
+            public KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception {
+                int count = attemptsCount.incrementAndGet();
+                if (count <= 3) {
+                    return kp;
+                } else {
+                    return UserInteraction.super.resolveAuthPublicKeyIdentityAttempt(session);
+                }
+            }
+        };
+
+        try (SshClient client = setupTestClient()) {
+            client.setUserAuthFactories(
+                    Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory()));
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.setPublicKeyAuthenticationReporter(reporter);
+                session.setUserInteraction(ui);
+                for (int index = 1; index <= 5; index++) {
+                    session.addPublicKeyIdentity(kp);
+                }
+                AuthFuture auth = session.auth();
+                assertAuthenticationResult("Authenticating", auth, false);
+            } finally {
+                client.stop();
+            }
+        }
+
+        assertEquals("Mismatched invocation count", 1, exhaustedCount.getAndSet(0));
+        assertEquals("Mismatched retries count", 4 /* 3 attempts + null */, attemptsCount.getAndSet(0));
+    }
 }