浏览代码

Add support for partial success limit to prevent stack overflow on badly behaving servers

Add support for partial success limit to prevent stack overflow on badly behaving servers
Gert Driesen 8 年之前
父节点
当前提交
16004effcc
共有 21 个文件被更改,包括 1746 次插入147 次删除
  1. 56 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs
  2. 11 8
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs
  3. 190 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs
  4. 30 13
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs
  5. 24 9
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs
  6. 54 4
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs
  7. 175 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs
  8. 202 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs
  9. 159 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs
  10. 185 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs
  11. 140 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs
  12. 44 19
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs
  13. 16 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs
  14. 16 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs
  15. 62 13
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs
  16. 150 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs
  17. 42 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs
  18. 0 1
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs
  19. 8 0
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  20. 175 76
      src/Renci.SshNet/ClientAuthentication.cs
  21. 7 1
      src/Renci.SshNet/ServiceFactory.cs

+ 56 - 1
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs

@@ -12,9 +12,64 @@ namespace Renci.SshNet.Tests.Classes
         [TestInitialize]
         public void Init()
         {
-            _clientAuthentication = new ClientAuthentication();
+            _clientAuthentication = new ClientAuthentication(1);
         }
 
+        [TestMethod]
+        public void Ctor_PartialSuccessLimit_Zero()
+        {
+            const int partialSuccessLimit = 0;
+
+            try
+            {
+                new ClientAuthentication(partialSuccessLimit);
+                Assert.Fail();
+            }
+            catch (ArgumentOutOfRangeException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message);
+                Assert.AreEqual("partialSuccessLimit", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Ctor_PartialSuccessLimit_Negative()
+        {
+            var partialSuccessLimit = new Random().Next(int.MinValue, -1);
+
+            try
+            {
+                new ClientAuthentication(partialSuccessLimit);
+                Assert.Fail();
+            }
+            catch (ArgumentOutOfRangeException ex)
+            {
+                Assert.IsNull(ex.InnerException);
+                Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message);
+                Assert.AreEqual("partialSuccessLimit", ex.ParamName);
+            }
+        }
+
+        [TestMethod]
+        public void Ctor_PartialSuccessLimit_One()
+        {
+            const int partialSuccessLimit = 1;
+
+            var clientAuthentication = new ClientAuthentication(partialSuccessLimit);
+            Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit);
+        }
+
+        [TestMethod]
+        public void Ctor_PartialSuccessLimit_MaxValue()
+        {
+            const int partialSuccessLimit = int.MaxValue;
+
+            var clientAuthentication = new ClientAuthentication(partialSuccessLimit);
+            Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit);
+        }
+
+
         [TestMethod]
         public void AuthenticateShouldThrowArgumentNullExceptionWhenConnectionInfoIsNull()
         {

+ 11 - 8
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs

@@ -13,7 +13,8 @@ namespace Renci.SshNet.Tests.Classes
         internal Mock<IAuthenticationMethod> PasswordAuthenticationMethodMock { get; private set; }
         internal Mock<IAuthenticationMethod> PublicKeyAuthenticationMethodMock { get; private set; }
         internal Mock<IAuthenticationMethod> KeyboardInteractiveAuthenticationMethodMock { get; private set; }
-        internal ClientAuthentication ClientAuthentication { get; private set; }
+
+        protected abstract void SetupData();
 
         protected void CreateMocks()
         {
@@ -27,18 +28,20 @@ namespace Renci.SshNet.Tests.Classes
 
         protected abstract void SetupMocks();
 
+        protected virtual void Arrange()
+        {
+            SetupData();
+            CreateMocks();
+            SetupMocks();
+        }
+
         protected abstract void Act();
 
-        protected override void OnInit()
+        protected sealed override void OnInit()
         {
             base.OnInit();
 
-            // Arrange
-            CreateMocks();
-            SetupMocks();
-            ClientAuthentication = new ClientAuthentication();
-
-            // Act
+            Arrange();
             Act();
         }
 

+ 190 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs

@@ -0,0 +1,190 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o publickey
+    ///     o password
+    /// * Partial success limit is 2
+    /// 
+    ///                               none
+    ///                             (1=FAIL)
+    ///                                 |
+    ///        +------------------------+------------------------+
+    ///        |                        |                        |
+    ///    password      ◄--\       publickey            keyboard-interactive
+    ///    (7=SKIP)         |         (2=PS)
+    ///                     |           |
+    ///                     |        password
+    ///                     |         (3=PS)
+    ///                     |           |
+    ///                     |        password
+    ///                     |         (4=PS)
+    ///                     |           |
+    ///                     |       publickey
+    ///                     |         (5=PS)
+    ///                     |           |
+    ///                     \----   publickey
+    ///                              (6=SKIP)
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"password", "publickey", "keyboard-interactive"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 2 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.AllowedAuthentications)
+                                             .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 5 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.AllowedAuthentications)
+                                             .Returns(new[] {"publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 6: Record partial success limit reached exception, and skip password authentication method */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Name)
+                                             .Returns("publickey-partial1");
+
+            /* 7: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial1");
+            
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                Assert.Fail();
+            }
+            catch (SshAuthenticationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void AuthenticateOnPasswordAuthenticationMethodShouldHaveBeenInvokedTwice()
+        {
+            PasswordAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice()
+        {
+            PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void AuthenticateShouldThrowSshAuthenticationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Reached authentication attempt limit for method (password-partial1).", _actualException.Message);
+        }
+    }
+}

+ 30 - 13
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs

@@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
         private SshAuthenticationException _actualException;
 
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 1;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -21,23 +28,26 @@ namespace Renci.SshNet.Tests.Classes
             ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
                 .Returns(NoneAuthenticationMethodMock.Object);
 
-            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.Failure);
-            ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
-                            .Returns(new List<IAuthenticationMethod>
-                {
-                    PublicKeyAuthenticationMethodMock.Object,
-                    PasswordAuthenticationMethodMock.Object
-                });
             NoneAuthenticationMethodMock.InSequence(seq)
-                .Setup(p => p.AllowedAuthentications)
-                .Returns(new[] { "password" });
+                                        .Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] { "password" });
 
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
 
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.Failure);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.Failure);
             // obtain name for inclusion in SshAuthenticationException
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
 
@@ -46,11 +56,18 @@ namespace Renci.SshNet.Tests.Classes
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
             try
             {
-                ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
                 Assert.Fail();
             }
             catch (SshAuthenticationException ex)

+ 24 - 9
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs

@@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotSupported : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
         private SshAuthenticationException _actualException;
 
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 1;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -21,16 +28,17 @@ namespace Renci.SshNet.Tests.Classes
             ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
                 .Returns(NoneAuthenticationMethodMock.Object);
 
-            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.Failure);
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
             ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
-                            .Returns(new List<IAuthenticationMethod>
-                {
-                    PublicKeyAuthenticationMethodMock.Object
-                });
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PublicKeyAuthenticationMethodMock.Object
+                                  });
             NoneAuthenticationMethodMock.InSequence(seq)
-                .Setup(p => p.AllowedAuthentications)
-                .Returns(new[] { "password" });
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"password"});
 
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
 
@@ -39,11 +47,18 @@ namespace Renci.SshNet.Tests.Classes
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
             try
             {
-                ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
                 Assert.Fail();
             }
             catch (SshAuthenticationException ex)

+ 54 - 4
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs

@@ -4,9 +4,36 @@ using Moq;
 
 namespace Renci.SshNet.Tests.Classes
 {
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o password
+    ///     o publickey
+    ///     o keyboard-interactive
+    /// * Partial success limit is 1
+    /// * Scenario:
+    ///                           none
+    ///                         (1=FAIL)
+    ///                             |
+    ///             +------------------------------+
+    ///             |                              |
+    ///         publickey                       password
+    ///                                       (2=PARTIAL)
+    ///                                *----------------------*
+    ///                                |                      |
+    ///                       keyboard-interactive        publickey
+    ///                                                  (3=SUCCESS)
+    /// </summary>
     [TestClass]
     public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 1;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -18,6 +45,8 @@ namespace Renci.SshNet.Tests.Classes
             ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
                 .Returns(NoneAuthenticationMethodMock.Object);
 
+            /* 1 */
+
             NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
                 .Returns(AuthenticationResult.Failure);
             ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
@@ -27,29 +56,50 @@ namespace Renci.SshNet.Tests.Classes
                     PublicKeyAuthenticationMethodMock.Object,
                     KeyboardInteractiveAuthenticationMethodMock.Object,
                 });
-            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "publickey", "password" });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"publickey", "password"});
+
+            /* Enumerate supported authentication methods */
+
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
             KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
 
+            /* 2 */
+
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
                 .Returns(AuthenticationResult.PartialSuccess);
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications)
-                .Returns(new[] { "keyboard-interactive", "publickey" });
+                                            .Returns(new[] {"keyboard-interactive", "publickey"});
+
+            /* Enumerate supported authentication methods */
+
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
             KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
 
-            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success);
+            /* 3 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Success);
 
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
-            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
         }
     }
 }

+ 175 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs

@@ -0,0 +1,175 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o password
+    ///     o publickey
+    ///     o keyboard-interactive
+    /// * Partial success limit is 2
+    /// * Scenario:
+    ///                    none
+    ///                  (1=FAIL)
+    ///                     |
+    ///             +------------------------+
+    ///             |                        |
+    ///         publickey          keyboard-interactive
+    ///          (2=PS)             ^    (6=FAIL)
+    ///             |               |
+    ///         password            |
+    ///          (3=PS)             |
+    ///             |               |
+    ///         password            |
+    ///          (4=PS)             |
+    ///             |               |
+    ///         password            |
+    ///         (5=SKIP)            |
+    ///             +---------------+
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"publickey", "keyboard-interactive"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 2 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 5: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial");
+
+            /* 6 */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.Authenticate(SessionMock.Object))
+                                                       .Returns(AuthenticationResult.Failure);
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure");
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                Assert.Fail();
+            }
+            catch (SshAuthenticationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce()
+        {
+            KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once);
+        }
+
+        [TestMethod]
+        public void AuthenticateShouldThrowSshAuthenticationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message);
+        }
+    }
+}

+ 202 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs

@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o password
+    ///     o publickey
+    ///     o keyboard-interactive
+    /// * Partial success limit is 2
+    /// 
+    ///                    none
+    ///                  (1=FAIL)
+    ///                     |
+    ///             +-------------------+
+    ///             |                   |
+    ///         publickey      keyboard-interactive
+    ///          (2=PS)          ^   (6=PS)
+    ///             |            |      |
+    ///         password         |      +-----------+
+    ///          (3=PS)          |      |           |
+    ///             |            |  password     publickey
+    ///         password         |   (7=SKIP)    (8=FAIL)
+    ///          (4=PS)          |
+    ///             |            |
+    ///         password         |
+    ///         (5=SKIP)         |
+    ///             +------------+
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2 : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] { "publickey", "keyboard-interactive" });
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 2 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.AllowedAuthentications)
+                                             .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 5: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial1");
+
+            /* 6 */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.Authenticate(SessionMock.Object))
+                                                       .Returns(AuthenticationResult.PartialSuccess);
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.AllowedAuthentications)
+                                                       .Returns(new[] {"password", "publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 7: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial2");
+
+            /* 8 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Failure);
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                Assert.Fail();
+            }
+            catch (SshAuthenticationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce()
+        {
+            KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once);
+        }
+
+        [TestMethod]
+        public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice()
+        {
+            PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
+        }
+
+        [TestMethod]
+        public void AuthenticateShouldThrowSshAuthenticationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Permission denied (publickey).", _actualException.Message);
+        }
+    }
+}

+ 159 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs

@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o keyboard-interactive
+    ///     o password
+    ///     o publickey
+    /// * Partial success limit is 2
+    /// * Scenario:
+    ///                           none
+    ///                          (1=FAIL)
+    ///                             |
+    ///                         password
+    ///                       (2=PARTIAL)
+    ///                             |
+    ///             +------------------------------+
+    ///             |                              |
+    ///         password                       publickey
+    ///       (4=PARTIAL)                     (3=FAILURE)
+    ///             |
+    ///     keyboard-interactive
+    ///        (5=FAILURE)
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 2 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password", "publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 3 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Failure);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Name)
+                                             .Returns("publickey-failure");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"keyboard-interactive"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 5 */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.Authenticate(SessionMock.Object))
+                                                       .Returns(AuthenticationResult.Failure);
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure");
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                Assert.Fail();
+            }
+            catch (SshAuthenticationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce()
+        {
+            KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once);
+        }
+
+        [TestMethod]
+        public void AuthenticateShouldThrowSshAuthenticationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message);
+        }
+    }
+}

+ 185 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs

@@ -0,0 +1,185 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o password
+    ///     o publickey
+    ///     o keyboard-interactive
+    /// * Partial success limit is 2
+    /// 
+    ///                    none
+    ///                  (1=FAIL)
+    ///                     |
+    ///             +-------------------+
+    ///             |                   |
+    ///         publickey      keyboard-interactive
+    ///          (2=PS)          ^   (6=PS)
+    ///             |            |      |
+    ///         password         |      +-----------+
+    ///          (3=PS)          |      |           |
+    ///             |            |  password     publickey
+    ///         password         |   (7=SKIP)   (8=SUCCESS)
+    ///          (4=PS)          |
+    ///             |            |
+    ///         password         |
+    ///         (5=SKIP)         |
+    ///             +------------+
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"publickey", "keyboard-interactive"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 2 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.AllowedAuthentications)
+                                             .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 5: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial1");
+
+            /* 6 */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.Authenticate(SessionMock.Object))
+                                                       .Returns(AuthenticationResult.PartialSuccess);
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.AllowedAuthentications)
+                                                       .Returns(new[] {"password", "publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+
+            /* 7: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Name)
+                                            .Returns("password-partial2");
+
+            /* 8 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Success);
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+
+        [TestMethod]
+        public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce()
+        {
+            KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once);
+        }
+
+        [TestMethod]
+        public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice()
+        {
+            PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2));
+        }
+    }
+}

+ 140 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs

@@ -0,0 +1,140 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o keyboard-interactive
+    ///     o password
+    ///     o publickey
+    /// * Partial success limit is 2
+    /// * Scenario:
+    ///                           none
+    ///                          (1=FAIL)
+    ///                             |
+    ///                         password
+    ///                       (2=PARTIAL)
+    ///                             |
+    ///             +------------------------------+
+    ///             |                              |
+    ///         password                       publickey
+    ///       (4=PARTIAL)                     (3=FAILURE)
+    ///             |
+    ///     keyboard-interactive
+    ///        (5=FAILURE)
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 2 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password", "publickey"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 3 */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Failure);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Name)
+                                             .Returns("publickey-failure");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"keyboard-interactive"});
+
+            /* Enumerate supported authentication methods */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+
+            /* 5 */
+
+            KeyboardInteractiveAuthenticationMethodMock.InSequence(seq)
+                                                       .Setup(p => p.Authenticate(SessionMock.Object))
+                                                       .Returns(AuthenticationResult.Success);
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+
+        [TestMethod]
+        public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce()
+        {
+            KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once);
+        }
+    }
+}

+ 44 - 19
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs

@@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public class ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 3;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -16,42 +24,59 @@ namespace Renci.SshNet.Tests.Classes
             SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
 
             ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
-                .Returns(NoneAuthenticationMethodMock.Object);
-
-            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.Failure);
-            ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
-                            .Returns(new List<IAuthenticationMethod>
-                {
-                    KeyboardInteractiveAuthenticationMethodMock.Object,
-                    PasswordAuthenticationMethodMock.Object,
-                    PublicKeyAuthenticationMethodMock.Object
-                });
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      KeyboardInteractiveAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object,
+                                      PublicKeyAuthenticationMethodMock.Object
+                                  });
             NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" });
             KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
 
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.PartialSuccess);
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications)
-                .Returns(new[] { "password", "publickey" });
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password", "publickey"});
             KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive");
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
 
-            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Failure);
-            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Authenticate(SessionMock.Object))
+                                             .Returns(AuthenticationResult.Failure);
+            PublicKeyAuthenticationMethodMock.InSequence(seq)
+                                             .Setup(p => p.Name)
+                                             .Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.Success);
 
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
-            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
         }
     }
 }

+ 16 - 1
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs

@@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public class ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 1;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -53,9 +61,16 @@ namespace Renci.SshNet.Tests.Classes
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
-            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
         }
     }
 }

+ 16 - 1
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs

@@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes
     [TestClass]
     public class ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 1;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -47,9 +55,16 @@ namespace Renci.SshNet.Tests.Classes
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
-            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
         }
     }
 }

+ 62 - 13
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs

@@ -4,9 +4,33 @@ using Moq;
 
 namespace Renci.SshNet.Tests.Classes
 {
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o keyboard-interactive
+    ///     o password
+    ///     o publickey
+    /// * Partial success limit is 2
+    /// * Scenario:
+    ///                           none
+    ///                          (1=FAIL)
+    ///                             |
+    ///                         password
+    ///                       (2=PARTIAL)
+    ///                             |
+    ///                         password
+    ///                       (3=SUCCESS)
+    /// </summary>
     [TestClass]
     public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase
     {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 2;
+        }
+
         protected override void SetupMocks()
         {
             var seq = new MockSequence();
@@ -18,36 +42,61 @@ namespace Renci.SshNet.Tests.Classes
             ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
                 .Returns(NoneAuthenticationMethodMock.Object);
 
+            /* 1 */
+
             NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
                 .Returns(AuthenticationResult.Failure);
-            ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods)
-                            .Returns(new List<IAuthenticationMethod>
-                {
-                    PublicKeyAuthenticationMethodMock.Object,
-                    PasswordAuthenticationMethodMock.Object
-                });
-            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" });
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
 
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
 
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
-                .Returns(AuthenticationResult.PartialSuccess);
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications)
-                .Returns(new[] { "password" });
+            /* 2 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
             PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
             PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
 
-            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success);
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.Success);
 
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
             SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
         }
 
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
         protected override void Act()
         {
-            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+            _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
         }
     }
 }

+ 150 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs

@@ -0,0 +1,150 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// * ConnectionInfo provides the following authentication methods (in order):
+    ///     o publickey
+    ///     o password
+    /// * Partial success limit is 3
+    /// * Scenario:
+    ///                           none
+    ///                          (1=FAIL)
+    ///                             |
+    ///                         password
+    ///                       (2=PARTIAL)
+    ///                             |
+    ///                         password
+    ///                       (3=PARTIAL)
+    ///                             |
+    ///                         password
+    ///                       (4=PARTIAL)
+    ///                             |
+    ///                         password
+    ///                         (5=SKIP)
+    /// </summary>
+    [TestClass]
+    public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached : ClientAuthenticationTestBase
+    {
+        private int _partialSuccessLimit;
+        private ClientAuthentication _clientAuthentication;
+        private SshAuthenticationException _actualException;
+
+        protected override void SetupData()
+        {
+            _partialSuccessLimit = 3;
+        }
+
+        protected override void SetupMocks()
+        {
+            var seq = new MockSequence();
+
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+
+            ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod())
+                              .Returns(NoneAuthenticationMethodMock.Object);
+
+            /* 1 */
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                                        .Returns(AuthenticationResult.Failure);
+            ConnectionInfoMock.InSequence(seq)
+                              .Setup(p => p.AuthenticationMethods)
+                              .Returns(new List<IAuthenticationMethod>
+                                  {
+                                      PublicKeyAuthenticationMethodMock.Object,
+                                      PasswordAuthenticationMethodMock.Object
+                                  });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                                        .Setup(p => p.AllowedAuthentications)
+                                        .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 2 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"password"});
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 3 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 4 */
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] { "password" });
+
+            /* Enumerate supported authentication methods */
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            /* 5: Record partial success limit reached exception, and skip password authentication method */
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("x_password_x");
+
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"));
+            SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"));
+        }
+
+        protected override void Arrange()
+        {
+            base.Arrange();
+
+            _clientAuthentication = new ClientAuthentication(_partialSuccessLimit);
+        }
+
+        protected override void Act()
+        {
+            try
+            {
+                _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+                Assert.Fail();
+            }
+            catch (SshAuthenticationException ex)
+            {
+                _actualException = ex;
+            }
+        }
+
+        [TestMethod]
+        public void AuthenticateShouldThrowSshAuthenticationException()
+        {
+            Assert.IsNotNull(_actualException);
+            Assert.IsNull(_actualException.InnerException);
+            Assert.AreEqual("Reached authentication attempt limit for method (x_password_x).",_actualException.Message);
+        }
+    }
+}

+ 42 - 0
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs

@@ -0,0 +1,42 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ServiceFactoryTest_CreateClientAuthentication
+    {
+        private ServiceFactory _serviceFactory;
+        private IClientAuthentication _actual;
+
+        private void Arrange()
+        {
+            _serviceFactory = new ServiceFactory();
+        }
+
+        [TestInitialize]
+        public void Initialize()
+        {
+            Arrange();
+            Act();
+        }
+
+        private void Act()
+        {
+            _actual = _serviceFactory.CreateClientAuthentication();
+        }
+
+        [TestMethod]
+        public void CreateClientAuthenticationShouldNotReturnNull()
+        {
+            Assert.IsNotNull(_actual);
+        }
+
+        [TestMethod]
+        public void ClientAuthenticationShouldHavePartialSuccessLimitOf5()
+        {
+            var clientAuthentication = _actual as ClientAuthentication;
+            Assert.IsNotNull(clientAuthentication);
+            Assert.AreEqual(5, clientAuthentication.PartialSuccessLimit);
+        }
+    }
+}

+ 0 - 1
src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs

@@ -90,6 +90,5 @@ namespace Renci.SshNet.Tests.Classes
             Assert.IsNotNull(_actual);
             Assert.AreSame(_sftpFileReaderMock.Object, _actual);
         }
-
     }
 }

+ 8 - 0
src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -139,11 +139,18 @@
     <Compile Include="Classes\ClientAuthenticationTestBase.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs" />
     <Compile Include="Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs" />
+    <Compile Include="Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs" />
     <Compile Include="Classes\Common\CountdownEventTest.cs" />
     <Compile Include="Classes\Common\CountdownEventTest_Dispose_NotSet.cs" />
     <Compile Include="Classes\Common\CountdownEventTest_Dispose_Set.cs" />
@@ -263,6 +270,7 @@
     <Compile Include="Classes\Common\PortForwardEventArgsTest.cs" />
     <Compile Include="Classes\Compression\ZlibTest.cs" />
     <Compile Include="Classes\ConnectionInfoTest.cs" />
+    <Compile Include="Classes\ServiceFactoryTest_CreateClientAuthentication.cs" />
     <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs" />
     <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsAlmostSixTimesGreaterThanChunkSize.cs" />
     <Compile Include="Classes\ServiceFactoryTest_CreateSftpFileReader_FileSizeIsEqualToChunkSize.cs" />

+ 175 - 76
src/Renci.SshNet/ClientAuthentication.cs

@@ -6,6 +6,40 @@ namespace Renci.SshNet
 {
     internal class ClientAuthentication : IClientAuthentication
     {
+        private readonly int _partialSuccessLimit;
+
+        /// <summary>
+        /// Initializes a new <see cref="ClientAuthentication"/> instance.
+        /// </summary>
+        /// <param name="partialSuccessLimit">The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="partialSuccessLimit"/> is less than one.</exception>
+        public ClientAuthentication(int partialSuccessLimit)
+        {
+            if (partialSuccessLimit < 1)
+                throw new ArgumentOutOfRangeException("partialSuccessLimit", "Cannot be less than one.");
+
+            _partialSuccessLimit = partialSuccessLimit;
+        }
+
+        /// <summary>
+        /// Gets the number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can
+        /// result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
+        /// </summary>
+        /// <value>
+        /// The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result
+        /// in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
+        /// </value>
+        internal int PartialSuccessLimit
+        {
+            get { return _partialSuccessLimit; }
+        }
+
+        /// <summary>
+        /// Attempts to authentication for a given <see cref="ISession"/> using the <see cref="IConnectionInfoInternal.AuthenticationMethods"/>
+        /// of the specified <see cref="IConnectionInfoInternal"/>.
+        /// </summary>
+        /// <param name="connectionInfo">A <see cref="IConnectionInfoInternal"/> to use for authenticating.</param>
+        /// <param name="session">The <see cref="ISession"/> for which to perform authentication.</param>
         public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session)
         {
             if (connectionInfo == null)
@@ -44,10 +78,10 @@ namespace Renci.SshNet
             }
         }
 
-        private static bool TryAuthenticate(ISession session,
-                                            AuthenticationState authenticationState,
-                                            string[] allowedAuthenticationMethods,
-                                            ref SshAuthenticationException authenticationException)
+        private bool TryAuthenticate(ISession session,
+                                     AuthenticationState authenticationState,
+                                     string[] allowedAuthenticationMethods,
+                                     ref SshAuthenticationException authenticationException)
         {
             if (allowedAuthenticationMethods.Length == 0)
             {
@@ -58,40 +92,39 @@ namespace Renci.SshNet
             // we want to try authentication methods in the order in which they were
             // passed in the ctor, not the order in which the SSH server returns
             // the allowed authentication methods
-            var matchingAuthenticationMethods = GetAllowedAuthenticationMethodsThatAreSupported(authenticationState, allowedAuthenticationMethods);
+            var matchingAuthenticationMethods = authenticationState.GetSupportedAuthenticationMethods(allowedAuthenticationMethods);
             if (matchingAuthenticationMethods.Count == 0)
             {
-                authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", string.Join(",", allowedAuthenticationMethods)));
+                authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).",
+                                                                                       string.Join(",", allowedAuthenticationMethods)));
                 return false;
             }
 
-            foreach (var authenticationMethod in GetOrderedAuthenticationMethods(authenticationState, matchingAuthenticationMethods))
+            foreach (var authenticationMethod in authenticationState.GetActiveAuthenticationMethods(matchingAuthenticationMethods))
             {
-                if (authenticationState.FailedAuthenticationMethods.Contains(authenticationMethod))
-                    continue;
-
-                // when the authentication method was previously executed, then skip the authentication
-                // method as long as there's another authentication method to try; this is done to avoid
-                // a stack overflow for servers that do not update the list of allowed authentication
+                // guard against a stack overlow for servers that do not update the list of allowed authentication
                 // methods after a partial success
-
-                if (!authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod))
+                if (authenticationState.GetPartialSuccessCount(authenticationMethod) >= _partialSuccessLimit)
                 {
-                    // update state to reflect previosuly executed authentication methods
-                    authenticationState.ExecutedAuthenticationMethods.Add(authenticationMethod);
+                    // TODO Get list of all authentication methods that have reached the partial success limit?
+
+                    authenticationException = new SshAuthenticationException(string.Format("Reached authentication attempt limit for method ({0}).",
+                                                                                           authenticationMethod.Name));
+                    continue;
                 }
 
                 var authenticationResult = authenticationMethod.Authenticate(session);
                 switch (authenticationResult)
                 {
                     case AuthenticationResult.PartialSuccess:
+                        authenticationState.RecordPartialSuccess(authenticationMethod);
                         if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications, ref authenticationException))
                         {
                             authenticationResult = AuthenticationResult.Success;
                         }
                         break;
                     case AuthenticationResult.Failure:
-                        authenticationState.FailedAuthenticationMethods.Add(authenticationMethod);
+                        authenticationState.RecordFailure(authenticationMethod);
                         authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name));
                         break;
                     case AuthenticationResult.Success:
@@ -106,85 +139,151 @@ namespace Renci.SshNet
             return false;
         }
 
-        private static List<IAuthenticationMethod> GetAllowedAuthenticationMethodsThatAreSupported(AuthenticationState authenticationState,
-                                                                                                   string[] allowedAuthenticationMethods)
+        private class AuthenticationState
         {
-            var result = new List<IAuthenticationMethod>();
-
-            foreach (var supportedAuthenticationMethod in authenticationState.SupportedAuthenticationMethods)
-            {
-                var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name;
+            private readonly IList<IAuthenticationMethod> _supportedAuthenticationMethods;
 
-                for (var i = 0; i < allowedAuthenticationMethods.Length; i++)
-                {
-                    if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod)
-                    {
-                        result.Add(supportedAuthenticationMethod);
-                        break;
-                    }
-                }
-            }
+            /// <summary>
+            /// Records if a given <see cref="IAuthenticationMethod"/> has been tried, and how many times this resulted
+            /// in <see cref="AuthenticationResult.PartialSuccess"/>.
+            /// </summary>
+            /// <remarks>
+            /// When there's no entry for a given <see cref="IAuthenticationMethod"/>, then it was never tried.
+            /// </remarks>
+            private readonly Dictionary<IAuthenticationMethod, int> _authenticationMethodPartialSuccessRegister;
 
-            return result;
-        }
+            /// <summary>
+            /// Holds the list of authentications methods that failed.
+            /// </summary>
+            private readonly List<IAuthenticationMethod> _failedAuthenticationMethods;
 
-        private static IEnumerable<IAuthenticationMethod> GetOrderedAuthenticationMethods(AuthenticationState authenticationState, List<IAuthenticationMethod> matchingAuthenticationMethods)
-        {
-            var skippedAuthenticationMethods = new List<IAuthenticationMethod>();
+            public AuthenticationState(IList<IAuthenticationMethod> supportedAuthenticationMethods)
+            {
+                _supportedAuthenticationMethods = supportedAuthenticationMethods;
+                _failedAuthenticationMethods = new List<IAuthenticationMethod>();
+                _authenticationMethodPartialSuccessRegister = new Dictionary<IAuthenticationMethod, int>();
+            }
 
-            for (var i = 0; i < matchingAuthenticationMethods.Count; i++)
+            /// <summary>
+            /// Records a <see cref="AuthenticationResult.Failure"/> authentication attempt for the specified
+            /// <see cref="IAuthenticationMethod"/> .
+            /// </summary>
+            /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
+            public void RecordFailure(IAuthenticationMethod authenticationMethod)
             {
-                var authenticationMethod = matchingAuthenticationMethods[i];
+                _failedAuthenticationMethods.Add(authenticationMethod);
+            }
 
-                if (authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod))
+            /// <summary>
+            /// Records a <see cref="AuthenticationResult.PartialSuccess"/> authentication attempt for the specified
+            /// <see cref="IAuthenticationMethod"/> .
+            /// </summary>
+            /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
+            public void RecordPartialSuccess(IAuthenticationMethod authenticationMethod)
+            {
+                int partialSuccessCount;
+                if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount))
                 {
-                    skippedAuthenticationMethods.Add(authenticationMethod);
-                    continue;
+                    _authenticationMethodPartialSuccessRegister[authenticationMethod] = ++partialSuccessCount;
+                }
+                else
+                {
+                    _authenticationMethodPartialSuccessRegister.Add(authenticationMethod, 1);
                 }
-
-                yield return authenticationMethod;
             }
 
-            foreach (var authenticationMethod in skippedAuthenticationMethods)
-                yield return authenticationMethod;
-        }
-
-        private class AuthenticationState
-        {
-            private readonly IList<IAuthenticationMethod> _supportedAuthenticationMethods;
-
-            public AuthenticationState(IList<IAuthenticationMethod> supportedAuthenticationMethods)
+            /// <summary>
+            /// Returns the number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
+            /// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
+            /// </summary>
+            /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/>.</param>
+            /// <returns>
+            /// The number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
+            /// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
+            /// </returns>
+            public int GetPartialSuccessCount(IAuthenticationMethod authenticationMethod)
             {
-                _supportedAuthenticationMethods = supportedAuthenticationMethods;
-                ExecutedAuthenticationMethods = new List<IAuthenticationMethod>();
-                FailedAuthenticationMethods = new List<IAuthenticationMethod>();
+                int partialSuccessCount;
+                if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount))
+                {
+                    return partialSuccessCount;
+                }
+                return 0;
             }
 
             /// <summary>
-            /// Gets the list of authentication methods that were previously executed.
+            /// Returns a list of supported authentication methods that match one of the specified allowed authentication
+            /// methods.
             /// </summary>
-            /// <value>
-            /// The list of authentication methods that were previously executed.
-            /// </value>
-            public IList<IAuthenticationMethod> ExecutedAuthenticationMethods { get; private set; }
+            /// <param name="allowedAuthenticationMethods">A list of allowed authentication methods.</param>
+            /// <returns>
+            /// A list of supported authentication methods that match one of the specified allowed authentication methods.
+            /// </returns>
+            /// <remarks>
+            /// The authentication methods are returned in the order in which they were specified in the list that was
+            /// used to initialize the current <see cref="AuthenticationState"/> instance.
+            /// </remarks>
+            public List<IAuthenticationMethod> GetSupportedAuthenticationMethods(string[] allowedAuthenticationMethods)
+            {
+                var result = new List<IAuthenticationMethod>();
 
-            /// <summary>
-            /// Gets the list of authentications methods that failed.
-            /// </summary>
-            /// <value>
-            /// The list of authentications methods that failed.
-            /// </value>
-            public IList<IAuthenticationMethod> FailedAuthenticationMethods { get; private set; }
+                foreach (var supportedAuthenticationMethod in _supportedAuthenticationMethods)
+                {
+                    var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name;
+
+                    for (var i = 0; i < allowedAuthenticationMethods.Length; i++)
+                    {
+                        if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod)
+                        {
+                            result.Add(supportedAuthenticationMethod);
+                            break;
+                        }
+                    }
+                }
+
+                return result;
+            }
 
             /// <summary>
-            /// Gets the list of supported authentication methods.
+            /// Returns the authentication methods from the specified list that have not yet failed.
             /// </summary>
-            /// <value>
-            /// The list of supported authentication methods.
-            /// </value>
-            public IList<IAuthenticationMethod> SupportedAuthenticationMethods
+            /// <param name="matchingAuthenticationMethods">A list of authentication methods.</param>
+            /// <returns>
+            /// The authentication methods from <paramref name="matchingAuthenticationMethods"/> that have not yet failed.
+            /// </returns>
+            /// <remarks>
+            /// <para>
+            /// This method first returns the authentication methods that have not yet been executed, and only then
+            /// returns those for which an authentication attempt resulted in a <see cref="AuthenticationResult.PartialSuccess"/>.
+            /// </para>
+            /// <para>
+            /// Any <see cref="IAuthenticationMethod"/> that has failed is skipped.
+            /// </para>
+            /// </remarks>
+            public IEnumerable<IAuthenticationMethod> GetActiveAuthenticationMethods(List<IAuthenticationMethod> matchingAuthenticationMethods)
             {
-                get { return _supportedAuthenticationMethods; }
+                var skippedAuthenticationMethods = new List<IAuthenticationMethod>();
+
+                for (var i = 0; i < matchingAuthenticationMethods.Count; i++)
+                {
+                    var authenticationMethod = matchingAuthenticationMethods[i];
+
+                    // skip authentication methods that have already failed
+                    if (_failedAuthenticationMethods.Contains(authenticationMethod))
+                        continue;
+
+                    // delay use of authentication methods that had a PartialSuccess result
+                    if (_authenticationMethodPartialSuccessRegister.ContainsKey(authenticationMethod))
+                    {
+                        skippedAuthenticationMethods.Add(authenticationMethod);
+                        continue;
+                    }
+
+                    yield return authenticationMethod;
+                }
+
+                foreach (var authenticationMethod in skippedAuthenticationMethods)
+                    yield return authenticationMethod;
             }
         }
     }

+ 7 - 1
src/Renci.SshNet/ServiceFactory.cs

@@ -15,6 +15,12 @@ namespace Renci.SshNet
     /// </summary>
     internal partial class ServiceFactory : IServiceFactory
     {
+        /// <summary>
+        /// Defines the number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/>
+        /// can result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
+        /// </summary>
+        private static int PartialSuccessLimit = 5;
+
         /// <summary>
         /// Creates a <see cref="IClientAuthentication"/>.
         /// </summary>
@@ -23,7 +29,7 @@ namespace Renci.SshNet
         /// </returns>
         public IClientAuthentication CreateClientAuthentication()
         {
-            return new ClientAuthentication();
+            return new ClientAuthentication(PartialSuccessLimit);
         }
 
         /// <summary>