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