Apache MINA sshd client: test & fix password authentication
Add tests for password and keyboard-interactive authentication. Implement password authentication; the default provided by sshd is non-interactive, which is not useful for JGit. Make sure the CredentialsProvider gets reset on successive password retrieval attempts. Otherwise it might always return the same non- accepted password from a secure storage. (That one was discovered by actually trying this via EGit; the JGit tests don't catch this.) Change the default order of authentication mechanisms to prefer password over keyboard-interactive. This is a mitigation for upstream bug SSHD-866.[1] Also include a fix for upstream bug SSHD-867.[2] [1] https://issues.apache.org/jira/projects/SSHD/issues/SSHD-866 [2] https://issues.apache.org/jira/projects/SSHD/issues/SSHD-867 Bug: 520927 Change-Id: I423e548f06d3b51531016cf08938c8bd7acaa2a9 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
parent
1316d43e51
commit
00b235f0b8
|
@ -22,10 +22,12 @@ Import-Package: org.apache.sshd.common;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.auth;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.auth;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.auth.gss;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.auth.gss;version="[2.0.0,2.1.0)",
|
||||||
|
org.apache.sshd.server.auth.keyboard;version="[2.0.0,2.1.0)",
|
||||||
|
org.apache.sshd.server.auth.password;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.command;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.command;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.session;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.session;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.shell;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.shell;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.subsystem;version="2.0.0",
|
org.apache.sshd.server.subsystem;version="[2.0.0,2.1.0)",
|
||||||
org.apache.sshd.server.subsystem.sftp;version="[2.0.0,2.1.0)",
|
org.apache.sshd.server.subsystem.sftp;version="[2.0.0,2.1.0)",
|
||||||
org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)",
|
org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)",
|
||||||
org.eclipse.jgit.lib;version="[5.2.0,5.3.0)",
|
org.eclipse.jgit.lib;version="[5.2.0,5.3.0)",
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
|
import org.apache.sshd.server.auth.gss.GSSAuthenticator;
|
||||||
import org.apache.sshd.server.auth.gss.UserAuthGSS;
|
import org.apache.sshd.server.auth.gss.UserAuthGSS;
|
||||||
import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
|
import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
|
||||||
|
import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
|
||||||
import org.apache.sshd.server.command.AbstractCommandSupport;
|
import org.apache.sshd.server.command.AbstractCommandSupport;
|
||||||
import org.apache.sshd.server.command.Command;
|
import org.apache.sshd.server.command.Command;
|
||||||
import org.apache.sshd.server.session.ServerSession;
|
import org.apache.sshd.server.session.ServerSession;
|
||||||
|
@ -184,14 +186,18 @@ protected Boolean doAuth(Buffer buffer, boolean initial)
|
||||||
|
|
||||||
private List<NamedFactory<UserAuth>> getAuthFactories() {
|
private List<NamedFactory<UserAuth>> getAuthFactories() {
|
||||||
List<NamedFactory<UserAuth>> authentications = new ArrayList<>();
|
List<NamedFactory<UserAuth>> authentications = new ArrayList<>();
|
||||||
authentications.add(
|
|
||||||
ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
|
|
||||||
authentications.add(new UserAuthGSSFactory() {
|
authentications.add(new UserAuthGSSFactory() {
|
||||||
@Override
|
@Override
|
||||||
public UserAuth create() {
|
public UserAuth create() {
|
||||||
return new FakeUserAuthGSS();
|
return new FakeUserAuthGSS();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
authentications.add(
|
||||||
|
ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
|
||||||
|
authentications.add(
|
||||||
|
ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY);
|
||||||
|
authentications.add(
|
||||||
|
ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY);
|
||||||
return authentications;
|
return authentications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,6 +286,30 @@ public void addHostKey(@NonNull Path key, boolean inFront)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable password authentication. The server will accept the test user's
|
||||||
|
* name, converted to all upper-case, as password.
|
||||||
|
*/
|
||||||
|
public void enablePasswordAuthentication() {
|
||||||
|
server.setPasswordAuthenticator((user, pwd, session) -> {
|
||||||
|
return testUser.equals(user)
|
||||||
|
&& testUser.toUpperCase(Locale.ROOT).equals(pwd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable keyboard-interactive authentication. The server will accept the
|
||||||
|
* test user's name, converted to all upper-case, as password.
|
||||||
|
*/
|
||||||
|
public void enableKeyboardInteractiveAuthentication() {
|
||||||
|
server.setPasswordAuthenticator((user, pwd, session) -> {
|
||||||
|
return testUser.equals(user)
|
||||||
|
&& testUser.toUpperCase(Locale.ROOT).equals(pwd);
|
||||||
|
});
|
||||||
|
server.setKeyboardInteractiveAuthenticator(
|
||||||
|
DefaultKeyboardInteractiveAuthenticator.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the test server, listening on a random port.
|
* Starts the test server, listening on a random port.
|
||||||
*
|
*
|
||||||
|
|
|
@ -44,6 +44,7 @@ knownHostsUnknownKeyPrompt=Accept and store this key, and continue connecting?
|
||||||
knownHostsUnknownKeyType=Cannot read server key from known hosts file {0}; line {1}
|
knownHostsUnknownKeyType=Cannot read server key from known hosts file {0}; line {1}
|
||||||
knownHostsUserAskCreationMsg=File {0} does not exist.
|
knownHostsUserAskCreationMsg=File {0} does not exist.
|
||||||
knownHostsUserAskCreationPrompt=Create file {0} ?
|
knownHostsUserAskCreationPrompt=Create file {0} ?
|
||||||
|
passwordPrompt=Password
|
||||||
proxyCannotAuthenticate=Cannot authenticate to proxy {0}
|
proxyCannotAuthenticate=Cannot authenticate to proxy {0}
|
||||||
proxyHttpFailure=HTTP Proxy connection to {0} failed with code {1}: {2}
|
proxyHttpFailure=HTTP Proxy connection to {0} failed with code {1}: {2}
|
||||||
proxyHttpInvalidUserName=HTTP proxy connection {0} with invalid user name; must not contain colons: {1}
|
proxyHttpInvalidUserName=HTTP proxy connection {0} with invalid user name; must not contain colons: {1}
|
||||||
|
|
|
@ -60,6 +60,22 @@ public class JGitHostConfigEntry extends HostConfigEntry {
|
||||||
|
|
||||||
private Map<String, List<String>> multiValuedOptions;
|
private Map<String, List<String>> multiValuedOptions;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProperty(String name, String defaultValue) {
|
||||||
|
// Upstream bug fix (SSHD-867): if there are _no_ properties at all, the
|
||||||
|
// super implementation returns always null even if a default value is
|
||||||
|
// given.
|
||||||
|
//
|
||||||
|
// See https://issues.apache.org/jira/projects/SSHD/issues/SSHD-867
|
||||||
|
//
|
||||||
|
// TODO: remove this override once we're based on sshd > 2.1.0
|
||||||
|
Map<String, String> properties = getProperties();
|
||||||
|
if (properties == null || properties.isEmpty()) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return super.getProperty(name, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the multi-valued options.
|
* Sets the multi-valued options.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
|
* and other copyright owners as documented in the project's IP log.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available
|
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which
|
||||||
|
* accompanies this distribution, is reproduced below, and is
|
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or
|
||||||
|
* without modification, are permitted provided that the following
|
||||||
|
* conditions are met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above
|
||||||
|
* copyright notice, this list of conditions and the following
|
||||||
|
* disclaimer in the documentation and/or other materials provided
|
||||||
|
* with the distribution.
|
||||||
|
*
|
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the
|
||||||
|
* names of its contributors may be used to endorse or promote
|
||||||
|
* products derived from this software without specific prior
|
||||||
|
* written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package org.eclipse.jgit.internal.transport.sshd;
|
||||||
|
|
||||||
|
import org.apache.sshd.client.auth.AbstractUserAuthFactory;
|
||||||
|
import org.apache.sshd.client.auth.UserAuth;
|
||||||
|
import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A customized {@link UserAuthPasswordFactory} that creates instance of
|
||||||
|
* {@link JGitPasswordAuthentication}.
|
||||||
|
*/
|
||||||
|
public class JGitPasswordAuthFactory extends AbstractUserAuthFactory {
|
||||||
|
|
||||||
|
/** The singleton {@link JGitPasswordAuthFactory}. */
|
||||||
|
public static final JGitPasswordAuthFactory INSTANCE = new JGitPasswordAuthFactory();
|
||||||
|
|
||||||
|
private JGitPasswordAuthFactory() {
|
||||||
|
super(UserAuthPasswordFactory.NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserAuth create() {
|
||||||
|
return new JGitPasswordAuthentication();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
|
||||||
|
* and other copyright owners as documented in the project's IP log.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available
|
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which
|
||||||
|
* accompanies this distribution, is reproduced below, and is
|
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
*
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or
|
||||||
|
* without modification, are permitted provided that the following
|
||||||
|
* conditions are met:
|
||||||
|
*
|
||||||
|
* - Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* - Redistributions in binary form must reproduce the above
|
||||||
|
* copyright notice, this list of conditions and the following
|
||||||
|
* disclaimer in the documentation and/or other materials provided
|
||||||
|
* with the distribution.
|
||||||
|
*
|
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the
|
||||||
|
* names of its contributors may be used to endorse or promote
|
||||||
|
* products derived from this software without specific prior
|
||||||
|
* written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package org.eclipse.jgit.internal.transport.sshd;
|
||||||
|
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
|
import org.apache.sshd.client.ClientAuthenticationManager;
|
||||||
|
import org.apache.sshd.client.auth.keyboard.UserInteraction;
|
||||||
|
import org.apache.sshd.client.auth.password.UserAuthPassword;
|
||||||
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A password authentication handler that uses the {@link JGitUserInteraction}
|
||||||
|
* to ask the user for the password. It also respects the
|
||||||
|
* {@code NumberOfPasswordPrompts} ssh config.
|
||||||
|
*/
|
||||||
|
public class JGitPasswordAuthentication extends UserAuthPassword {
|
||||||
|
|
||||||
|
private int maxAttempts;
|
||||||
|
|
||||||
|
private int attempts;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ClientSession session, String service) throws Exception {
|
||||||
|
super.init(session, service);
|
||||||
|
maxAttempts = Math.max(1,
|
||||||
|
session.getIntProperty(
|
||||||
|
ClientAuthenticationManager.PASSWORD_PROMPTS,
|
||||||
|
ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS));
|
||||||
|
attempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean sendAuthDataRequest(ClientSession session, String service)
|
||||||
|
throws Exception {
|
||||||
|
if (++attempts > maxAttempts) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UserInteraction interaction = session.getUserInteraction();
|
||||||
|
if (!interaction.isInteractionAllowed(session)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String password = getPassword(session, interaction);
|
||||||
|
if (password == null) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
// sendPassword takes a buffer as first argument, but actually doesn't
|
||||||
|
// use it and creates its own buffer...
|
||||||
|
sendPassword(null, session, password, password);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPassword(ClientSession session,
|
||||||
|
UserInteraction interaction) {
|
||||||
|
String[] results = interaction.interactive(session, null, null, "", //$NON-NLS-1$
|
||||||
|
new String[] { SshdText.get().passwordPrompt },
|
||||||
|
new boolean[] { false });
|
||||||
|
return (results == null || results.length == 0) ? null : results[0];
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,23 +105,6 @@ public HostConfigEntry resolveEffectiveHost(String host, int port,
|
||||||
String username) throws IOException {
|
String username) throws IOException {
|
||||||
HostEntry entry = configFile.lookup(host, port, username);
|
HostEntry entry = configFile.lookup(host, port, username);
|
||||||
JGitHostConfigEntry config = new JGitHostConfigEntry();
|
JGitHostConfigEntry config = new JGitHostConfigEntry();
|
||||||
String hostName = entry.getValue(SshConstants.HOST_NAME);
|
|
||||||
if (hostName == null || hostName.isEmpty()) {
|
|
||||||
hostName = host;
|
|
||||||
}
|
|
||||||
config.setHostName(hostName);
|
|
||||||
config.setHost(SshdSocketAddress.isIPv6Address(hostName) ? "" : hostName); //$NON-NLS-1$
|
|
||||||
String user = username != null && !username.isEmpty() ? username
|
|
||||||
: entry.getValue(SshConstants.USER);
|
|
||||||
if (user == null || user.isEmpty()) {
|
|
||||||
user = configFile.getLocalUserName();
|
|
||||||
}
|
|
||||||
config.setUsername(user);
|
|
||||||
int p = port >= 0 ? port : positive(entry.getValue(SshConstants.PORT));
|
|
||||||
config.setPort(p >= 0 ? p : SshConstants.SSH_DEFAULT_PORT);
|
|
||||||
config.setIdentities(entry.getValues(SshConstants.IDENTITY_FILE));
|
|
||||||
config.setIdentitiesOnly(
|
|
||||||
flag(entry.getValue(SshConstants.IDENTITIES_ONLY)));
|
|
||||||
// Apache MINA conflates all keys, even multi-valued ones, in one map
|
// Apache MINA conflates all keys, even multi-valued ones, in one map
|
||||||
// and puts multiple values separated by commas in one string. See
|
// and puts multiple values separated by commas in one string. See
|
||||||
// the javadoc on HostConfigEntry.
|
// the javadoc on HostConfigEntry.
|
||||||
|
@ -135,6 +118,28 @@ public HostConfigEntry resolveEffectiveHost(String host, int port,
|
||||||
config.setProperties(allOptions);
|
config.setProperties(allOptions);
|
||||||
// The following is an extension from JGitHostConfigEntry
|
// The following is an extension from JGitHostConfigEntry
|
||||||
config.setMultiValuedOptions(entry.getMultiValuedOptions());
|
config.setMultiValuedOptions(entry.getMultiValuedOptions());
|
||||||
|
// Also make sure the underlying properties are set
|
||||||
|
String hostName = entry.getValue(SshConstants.HOST_NAME);
|
||||||
|
if (hostName == null || hostName.isEmpty()) {
|
||||||
|
hostName = host;
|
||||||
|
}
|
||||||
|
config.setHostName(hostName);
|
||||||
|
config.setProperty(SshConstants.HOST_NAME, hostName);
|
||||||
|
config.setHost(SshdSocketAddress.isIPv6Address(hostName) ? "" : hostName); //$NON-NLS-1$
|
||||||
|
String user = username != null && !username.isEmpty() ? username
|
||||||
|
: entry.getValue(SshConstants.USER);
|
||||||
|
if (user == null || user.isEmpty()) {
|
||||||
|
user = configFile.getLocalUserName();
|
||||||
|
}
|
||||||
|
config.setUsername(user);
|
||||||
|
config.setProperty(SshConstants.USER, user);
|
||||||
|
int p = port >= 0 ? port : positive(entry.getValue(SshConstants.PORT));
|
||||||
|
config.setPort(p >= 0 ? p : SshConstants.SSH_DEFAULT_PORT);
|
||||||
|
config.setProperty(SshConstants.PORT,
|
||||||
|
Integer.toString(config.getPort()));
|
||||||
|
config.setIdentities(entry.getValues(SshConstants.IDENTITY_FILE));
|
||||||
|
config.setIdentitiesOnly(
|
||||||
|
flag(entry.getValue(SshConstants.IDENTITIES_ONLY)));
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,9 +45,13 @@
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.apache.sshd.client.auth.keyboard.UserInteraction;
|
import org.apache.sshd.client.auth.keyboard.UserInteraction;
|
||||||
import org.apache.sshd.client.session.ClientSession;
|
import org.apache.sshd.client.session.ClientSession;
|
||||||
|
import org.apache.sshd.common.session.Session;
|
||||||
|
import org.apache.sshd.common.session.SessionListener;
|
||||||
import org.eclipse.jgit.transport.CredentialItem;
|
import org.eclipse.jgit.transport.CredentialItem;
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||||
import org.eclipse.jgit.transport.SshConstants;
|
import org.eclipse.jgit.transport.SshConstants;
|
||||||
|
@ -61,6 +65,12 @@ public class JGitUserInteraction implements UserInteraction {
|
||||||
|
|
||||||
private final CredentialsProvider provider;
|
private final CredentialsProvider provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to reset the JGit credentials provider if we have repeated
|
||||||
|
* attempts.
|
||||||
|
*/
|
||||||
|
private final Map<Session, SessionListener> ongoing = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link JGitUserInteraction} for interactive password input
|
* Creates a new {@link JGitUserInteraction} for interactive password input
|
||||||
* based on the given {@link CredentialsProvider}.
|
* based on the given {@link CredentialsProvider}.
|
||||||
|
@ -74,13 +84,13 @@ public JGitUserInteraction(CredentialsProvider provider) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isInteractionAllowed(ClientSession session) {
|
public boolean isInteractionAllowed(ClientSession session) {
|
||||||
return provider.isInteractive();
|
return provider != null && provider.isInteractive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] interactive(ClientSession session, String name,
|
public String[] interactive(ClientSession session, String name,
|
||||||
String instruction, String lang, String[] prompt, boolean[] echo) {
|
String instruction, String lang, String[] prompt, boolean[] echo) {
|
||||||
// This is keyboard-interactive authentication
|
// This is keyboard-interactive or password authentication
|
||||||
List<CredentialItem> items = new ArrayList<>();
|
List<CredentialItem> items = new ArrayList<>();
|
||||||
int numberOfHiddenInputs = 0;
|
int numberOfHiddenInputs = 0;
|
||||||
for (int i = 0; i < prompt.length; i++) {
|
for (int i = 0; i < prompt.length; i++) {
|
||||||
|
@ -120,6 +130,19 @@ public String[] interactive(ClientSession session, String name,
|
||||||
}
|
}
|
||||||
URIish uri = toURI(session.getUsername(),
|
URIish uri = toURI(session.getUsername(),
|
||||||
(InetSocketAddress) session.getConnectAddress());
|
(InetSocketAddress) session.getConnectAddress());
|
||||||
|
// Reset the provider for this URI if it's not the first attempt and we
|
||||||
|
// have hidden inputs. Otherwise add a session listener that will remove
|
||||||
|
// itself once authenticated.
|
||||||
|
if (numberOfHiddenInputs > 0) {
|
||||||
|
SessionListener listener = ongoing.get(session);
|
||||||
|
if (listener != null) {
|
||||||
|
provider.reset(uri);
|
||||||
|
} else {
|
||||||
|
listener = new SessionAuthMarker(ongoing);
|
||||||
|
ongoing.put(session, listener);
|
||||||
|
session.addSessionListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (provider.get(uri, items)) {
|
if (provider.get(uri, items)) {
|
||||||
return items.stream().map(i -> {
|
return items.stream().map(i -> {
|
||||||
if (i instanceof CredentialItem.Password) {
|
if (i instanceof CredentialItem.Password) {
|
||||||
|
@ -166,4 +189,31 @@ public static URIish toURI(String userName, InetSocketAddress remote) {
|
||||||
.setPort(port) //
|
.setPort(port) //
|
||||||
.setUser(userName);
|
.setUser(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SessionListener} that removes itself from the session when
|
||||||
|
* authentication is done or the session is closed.
|
||||||
|
*/
|
||||||
|
private static class SessionAuthMarker implements SessionListener {
|
||||||
|
|
||||||
|
private final Map<Session, SessionListener> registered;
|
||||||
|
|
||||||
|
public SessionAuthMarker(Map<Session, SessionListener> registered) {
|
||||||
|
this.registered = registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionEvent(Session session, SessionListener.Event event) {
|
||||||
|
if (event == SessionListener.Event.Authenticated) {
|
||||||
|
session.removeSessionListener(this);
|
||||||
|
registered.remove(session, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionClosed(Session session) {
|
||||||
|
session.removeSessionListener(this);
|
||||||
|
registered.remove(session, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ public static SshdText get() {
|
||||||
/***/ public String knownHostsUnknownKeyType;
|
/***/ public String knownHostsUnknownKeyType;
|
||||||
/***/ public String knownHostsUserAskCreationMsg;
|
/***/ public String knownHostsUserAskCreationMsg;
|
||||||
/***/ public String knownHostsUserAskCreationPrompt;
|
/***/ public String knownHostsUserAskCreationPrompt;
|
||||||
|
/***/ public String passwordPrompt;
|
||||||
/***/ public String proxyCannotAuthenticate;
|
/***/ public String proxyCannotAuthenticate;
|
||||||
/***/ public String proxyHttpFailure;
|
/***/ public String proxyHttpFailure;
|
||||||
/***/ public String proxyHttpInvalidUserName;
|
/***/ public String proxyHttpInvalidUserName;
|
||||||
|
|
|
@ -63,7 +63,6 @@
|
||||||
import org.apache.sshd.client.SshClient;
|
import org.apache.sshd.client.SshClient;
|
||||||
import org.apache.sshd.client.auth.UserAuth;
|
import org.apache.sshd.client.auth.UserAuth;
|
||||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||||
import org.apache.sshd.client.auth.password.UserAuthPasswordFactory;
|
|
||||||
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
|
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
|
||||||
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
|
||||||
import org.apache.sshd.common.NamedFactory;
|
import org.apache.sshd.common.NamedFactory;
|
||||||
|
@ -75,6 +74,7 @@
|
||||||
import org.eclipse.jgit.errors.TransportException;
|
import org.eclipse.jgit.errors.TransportException;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
|
import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
|
||||||
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
|
||||||
|
@ -465,21 +465,23 @@ private FilePasswordProvider createFilePasswordProvider(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the user authentication mechanisms (or rather, factories for them).
|
* Gets the user authentication mechanisms (or rather, factories for them).
|
||||||
* By default this returns gssapi-with-mic, public-key,
|
* By default this returns gssapi-with-mic, public-key, password, and
|
||||||
* keyboard-interactive, and password, in that order. The order is only
|
* keyboard-interactive, in that order. The order is only significant if the
|
||||||
* significant if the ssh config does <em>not</em> set
|
* ssh config does <em>not</em> set {@code PreferredAuthentications}; if it
|
||||||
* {@code PreferredAuthentications}; if it is set, the order defined there
|
* is set, the order defined there will be taken.
|
||||||
* will be taken.
|
|
||||||
*
|
*
|
||||||
* @return the non-empty list of factories.
|
* @return the non-empty list of factories.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private List<NamedFactory<UserAuth>> getUserAuthFactories() {
|
private List<NamedFactory<UserAuth>> getUserAuthFactories() {
|
||||||
|
// About the order of password and keyboard-interactive, see upstream
|
||||||
|
// bug https://issues.apache.org/jira/projects/SSHD/issues/SSHD-866 .
|
||||||
|
// Password auth doesn't have this problem.
|
||||||
return Collections.unmodifiableList(
|
return Collections.unmodifiableList(
|
||||||
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
|
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
|
||||||
JGitPublicKeyAuthFactory.INSTANCE,
|
JGitPublicKeyAuthFactory.INSTANCE,
|
||||||
UserAuthKeyboardInteractiveFactory.INSTANCE,
|
JGitPasswordAuthFactory.INSTANCE,
|
||||||
UserAuthPasswordFactory.INSTANCE));
|
UserAuthKeyboardInteractiveFactory.INSTANCE));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.errors.TransportException;
|
import org.eclipse.jgit.api.errors.TransportException;
|
||||||
import org.eclipse.jgit.transport.CredentialItem;
|
import org.eclipse.jgit.transport.CredentialItem;
|
||||||
|
@ -668,6 +669,137 @@ public void testEcDsaHostKey() throws Exception {
|
||||||
"IdentityFile " + privateKey1.getAbsolutePath());
|
"IdentityFile " + privateKey1.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordAuth() throws Exception {
|
||||||
|
server.enablePasswordAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasswordAuthSeveralTimes() throws Exception {
|
||||||
|
server.enablePasswordAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass", "wrongpass", TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testPasswordAuthWrongPassword() throws Exception {
|
||||||
|
server.enablePasswordAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass");
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testPasswordAuthNoPassword() throws Exception {
|
||||||
|
server.enablePasswordAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider();
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testPasswordAuthCorrectPasswordTooLate() throws Exception {
|
||||||
|
server.enablePasswordAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass", "wrongpass", "wrongpass",
|
||||||
|
TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyboardInteractiveAuth() throws Exception {
|
||||||
|
server.enableKeyboardInteractiveAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications keyboard-interactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyboardInteractiveAuthSeveralTimes() throws Exception {
|
||||||
|
server.enableKeyboardInteractiveAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass", "wrongpass", TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications keyboard-interactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testKeyboardInteractiveAuthWrongPassword() throws Exception {
|
||||||
|
server.enableKeyboardInteractiveAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass");
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications keyboard-interactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testKeyboardInteractiveAuthNoPassword() throws Exception {
|
||||||
|
server.enableKeyboardInteractiveAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider();
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications keyboard-interactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = TransportException.class)
|
||||||
|
public void testKeyboardInteractiveAuthCorrectPasswordTooLate()
|
||||||
|
throws Exception {
|
||||||
|
server.enableKeyboardInteractiveAuthentication();
|
||||||
|
TestCredentialsProvider provider = new TestCredentialsProvider(
|
||||||
|
"wrongpass", "wrongpass", "wrongpass",
|
||||||
|
TEST_USER.toUpperCase(Locale.ROOT));
|
||||||
|
cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
|
||||||
|
"Host git", //
|
||||||
|
"HostName localhost", //
|
||||||
|
"Port " + testPort, //
|
||||||
|
"User " + TEST_USER, //
|
||||||
|
"PreferredAuthentications keyboard-interactive");
|
||||||
|
}
|
||||||
|
|
||||||
@Theory
|
@Theory
|
||||||
public void testSshKeys(String keyName) throws Exception {
|
public void testSshKeys(String keyName) throws Exception {
|
||||||
// JSch fails on ECDSA 384/521 keys. Compare
|
// JSch fails on ECDSA 384/521 keys. Compare
|
||||||
|
|
Loading…
Reference in New Issue