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:48 UTC

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

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());