mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Added callback to request updated password for a user in case of USERAUTH_PASSWD_CHANGEREQ (Fixes #193)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user