IMLC.ME

Apache MINA SSHD - How to change password if current password expired

For security reason, some SSH server may require you to reset password in your first login, or expire your password after a certain period. It's easy if you SSH manually. However, if you want to SSH by a Java appliation, you may have a little trouble here.

I'm working on connecting to a remote SSH service. That is a server application that serves requests over SSH protocol. Somehow, I didn't figure out why, Apache MIINA SSHD didn't work with it by default.

What's the problem?

There are 3 steps to change the password:

  1. Enter current password
  2. Enter new password
  3. Reenter new password

When I connect to SSH server via MINA SSHD, I can type current password and new password. However, I didn't receive the prompt for step3 reenter new password.

Why does it happen

If you enable the debug log, you will see a few logs that quite suspected.

verifyTrialsCount(XXX)[XXX] cmd=XXX - 4 out of 3

And check out the source code: https://github.com/apache/mina-sshd/blob/a33bd5472092fbff55719bbe0c3fb9a9b46c121f/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserAuthKeyboardInteractive.java#L213

There is no doubt that it excceeded the limits of trial count and the step3 reenter new password was blocked.

MINA SSHD will try to authentiate with server in different ways:

  1. Password (0 out of 3)
  2. Public Key (1 out of 3)
  3. Keyboard Interactive - Current password ( 2 out of 3)
  4. Keyboard Interactive - New password (3 out of 3)
  5. Keyboard Interactive - Reenter new password (4 out of 3)

And the default max trial count is too low.

Look up at source code, we can easily figure out where the max trial count comes from:

maxTrials = CoreModuleProperties.PASSWORD_PROMPTS.getRequired(session);

if we can increase the number of maxTrials ...

How to fix it

Talk it cheap, show you the code. The key point is CoreModuleProperties.PASSWORD_PROMPTS.set(session, 4).

public class Main {

  public static void main(String[] args) throws IOException {
    SshClient client = SshClient.setUpDefaultClient();

    String currentPassword = "currentPassword123";
    String newPassword = "newPassword123";
    
    // Implement the UserInteraction interface to login SSH server in keyboard-interactive mode
    client.setUserInteraction(new UserInteraction() {
      @Override
      public String[] interactive(ClientSession session, String name, String instruction,
          String lang, String[] prompt, boolean[] echo) {
        if(prompt.length > 0) {
          
          String msg = prompt[0];

          if(msg.contains("Current password")) {
            return new String[]{currentPassword};
          }

          if(msg.contains("New password") || msg.contains("Reenter password")) {
            return new String[]{newPassword};
          }
        }

        throw new UnsupportedOperationException(
            "Unsupported interactive for prompt: {}" +
                String.join(",", prompt)
        );
      }

      @Override
      public String getUpdatedPassword(ClientSession session, String prompt, String lang) {
        return null;
      }
    });

    client.start();

    ConnectFuture connectFuture = client.connect("username", "example.com", 22);
    try (ClientSession session = connectFuture.verify(30, TimeUnit.SECONDS)
        .getSession()) {
      
      // Bump the max trial count
      CoreModuleProperties.PASSWORD_PROMPTS.set(session, 4);

      session.auth().verify(30, TimeUnit.SECONDS);

      ChannelShell shellChannel = session.createShellChannel();
      // ...
    }

  }

}