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

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

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));
+    }
 }