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 <[email protected]> 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)); + } }
