Added callback to request updated password for a user in case of USERAUTH_PASSWD_CHANGEREQ (Fixes #193)

This commit is contained in:
Jeroen van Erp
2016-03-18 14:29:03 +01:00
parent a2cccd5cef
commit eb8b7b51ca
6 changed files with 239 additions and 10 deletions

View File

@@ -57,6 +57,7 @@ import net.schmizz.sshj.userauth.method.AuthPassword;
import net.schmizz.sshj.userauth.method.AuthPublickey;
import net.schmizz.sshj.userauth.method.PasswordResponseProvider;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.userauth.password.Resource;
import net.schmizz.sshj.xfer.scp.SCPFileTransfer;
@@ -293,6 +294,22 @@ public class SSHClient
auth(username, new AuthPassword(pfinder), new AuthKeyboardInteractive(new PasswordResponseProvider(pfinder)));
}
/**
* Authenticate {@code username} using the {@code "password"} authentication method and as a fallback basic
* challenge-response authentication.
*
* @param username user to authenticate
* @param pfinder the {@link PasswordFinder} to use for authentication
* @param newPasswordProvider the {@link PasswordUpdateProvider} to use when a new password is being requested from the user.
*
* @throws UserAuthException in case of authentication failure
* @throws TransportException if there was a transport-layer error
*/
public void authPassword(String username, PasswordFinder pfinder, PasswordUpdateProvider newPasswordProvider)
throws UserAuthException, TransportException {
auth(username, new AuthPassword(pfinder, newPasswordProvider), new AuthKeyboardInteractive(new PasswordResponseProvider(pfinder)));
}
/**
* Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from some common
* locations on the file system. This method relies on {@code ~/.ssh/id_rsa} and {@code ~/.ssh/id_dsa}.
@@ -759,4 +776,4 @@ public class SSHClient
}
}
}
}

View File

@@ -15,12 +15,17 @@
*/
package net.schmizz.sshj.userauth.method;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.Message;
import net.schmizz.sshj.common.SSHPacket;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.password.AccountResource;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
import net.schmizz.sshj.userauth.password.Resource;
import static net.schmizz.sshj.common.Message.USERAUTH_REQUEST;
/** Implements the {@code password} authentication method. Password-change request handling is not currently supported. */
public class AuthPassword
@@ -28,9 +33,28 @@ public class AuthPassword
private final PasswordFinder pwdf;
private static final PasswordUpdateProvider nullProvider = new PasswordUpdateProvider() {
@Override
public char[] provideNewPassword(Resource<?> resource, String prompt) {
return null;
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
};
private final PasswordUpdateProvider newPasswordProvider;
public AuthPassword(PasswordFinder pwdf) {
this(pwdf, nullProvider);
}
public AuthPassword(PasswordFinder pwdf, PasswordUpdateProvider newPasswordProvider) {
super("password");
this.pwdf = pwdf;
this.newPasswordProvider = newPasswordProvider;
}
@Override
@@ -46,10 +70,23 @@ public class AuthPassword
@Override
public void handle(Message cmd, SSHPacket buf)
throws UserAuthException, TransportException {
if (cmd == Message.USERAUTH_60)
throw new UserAuthException("Password change request received; unsupported operation");
else
if (cmd == Message.USERAUTH_60 && newPasswordProvider != null) {
log.info("Received SSH_MSG_USERAUTH_PASSWD_CHANGEREQ.");
try {
String prompt = buf.readString();
buf.readString(); // lang-tag
AccountResource resource = makeAccountResource();
char[] newPassword = newPasswordProvider.provideNewPassword(resource, prompt);
SSHPacket sshPacket = super.buildReq().putBoolean(true).putSensitiveString(pwdf.reqPassword(resource)).putSensitiveString(newPassword);
params.getTransport().write(sshPacket);
} catch (Buffer.BufferException e) {
throw new TransportException(e);
}
} else if (cmd == Message.USERAUTH_60) {
throw new UserAuthException("Password change request received; unsupported operation (newPassword was 'null')");
} else {
super.handle(cmd, buf);
}
}
/**
@@ -58,7 +95,8 @@ public class AuthPassword
*/
@Override
public boolean shouldRetry() {
return pwdf.shouldRetry(makeAccountResource());
AccountResource accountResource = makeAccountResource();
return newPasswordProvider.shouldRetry(accountResource) || pwdf.shouldRetry(accountResource);
}
}
}

View File

@@ -0,0 +1,30 @@
package net.schmizz.sshj.userauth.password;
/**
* Callback that can be implemented to allow an application to provide an updated password for the 'auth-password'
* authentication method.
*/
public interface PasswordUpdateProvider {
/**
* Called with the prompt received from the SSH server. This should return the updated password for the user that is
* currently authenticating.
*
* @param resource The resource for which the updated password is being requested.
* @param prompt The password update prompt received from the SSH Server.
* @return The new password for the resource.
*/
char[] provideNewPassword(Resource<?> resource, String prompt);
/**
* If password turns out to be incorrect, indicates whether another call to {@link #reqPassword(Resource)} should be
* made.
* <p/>
* This method is geared at interactive implementations, and stub implementations may simply return {@code false}.
*
* @param resource the resource for which the updated password is being requested
*
* @return whether to retry requesting the updated password for a particular resource
*/
boolean shouldRetry(Resource<?> resource);
}

View File

@@ -6,13 +6,12 @@ import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.keyprovider.AbstractClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.command.ScpCommandFactory;
import org.apache.sshd.server.scp.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.shell.ProcessShellFactory;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
@@ -22,7 +21,6 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicBoolean;
/**

View File

@@ -0,0 +1,146 @@
package com.hierynomus.sshj.userauth.method;
import com.hierynomus.sshj.test.SshFixture;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
import net.schmizz.sshj.userauth.password.Resource;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.server.auth.UserAuth;
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
import org.apache.sshd.server.auth.password.UserAuthPassword;
import org.apache.sshd.server.session.ServerSession;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.io.IOException;
import java.util.Collections;
import java.util.Stack;
import static org.hamcrest.MatcherAssert.assertThat;
public class AuthPasswordTest {
@Rule
public SshFixture fixture = new SshFixture(false);
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setPasswordAuthenticator() throws IOException {
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
@Override
public String getName() {
return "password";
}
@Override
public UserAuth create() {
return new UserAuthPassword() {
@Override
protected Boolean handleClientPasswordChangeRequest(Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) throws Exception {
return checkPassword(buffer, session, username, newPassword);
}
};
}
}));
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
@Override
public boolean authenticate(String username, String password, ServerSession session) {
if (password.equals("changeme")) {
throw new PasswordChangeRequiredException("Password was changeme", "Please provide your updated password", "en_US");
} else {
return password.equals(username);
}
}
});
fixture.getServer().start();
}
@Test
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient();
expectedException.expect(UserAuthException.class);
sshClient.authPassword("jeroen", "changeme");
}
@Test
public void shouldHandlePasswordChange() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient();
sshClient.authPassword("jeroen", new PasswordFinder() {
@Override
public char[] reqPassword(Resource<?> resource) {
return "changeme".toCharArray();
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
}, new StaticPasswordUpdateProvider("jeroen"));
assertThat("Should be authenticated", sshClient.isAuthenticated());
}
@Test
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient();
expectedException.expect(UserAuthException.class);
sshClient.authPassword("jeroen", new PasswordFinder() {
@Override
public char[] reqPassword(Resource<?> resource) {
return "changeme".toCharArray();
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
}, new StaticPasswordUpdateProvider("bad"));
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
}
@Test
public void shouldHandlePasswordChangeWithWrongPasswordOnFirstAttempt() throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient();
sshClient.authPassword("jeroen", new PasswordFinder() {
@Override
public char[] reqPassword(Resource<?> resource) {
return "changeme".toCharArray();
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return false;
}
}, new StaticPasswordUpdateProvider("bad", "jeroen"));
assertThat("Should have been authenticated", sshClient.isAuthenticated());
}
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
private Stack<String> newPasswords = new Stack<>();
public StaticPasswordUpdateProvider(String... newPasswords) {
for (int i = newPasswords.length - 1; i >= 0; i--) {
this.newPasswords.push(newPasswords[i]);
}
}
@Override
public char[] provideNewPassword(Resource<?> resource, String prompt) {
return newPasswords.pop().toCharArray();
}
@Override
public boolean shouldRetry(Resource<?> resource) {
return !newPasswords.isEmpty();
}
}
}