浏览代码

Introduce a partial success limit.
Added first batch of tests.

Gert Driesen 8 年之前
父节点
当前提交
14b654d0ab
共有 19 个文件被更改,包括 1210 次插入135 次删除
  1. 56 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs
  2. 11 8
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs
  3. 30 13
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs
  4. 15 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs
  5. 14 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs
  6. 16 1
      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. 144 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs
  10. 121 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs
  11. 44 19
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs
  12. 16 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs
  13. 16 1
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs
  14. 35 13
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs
  15. 112 0
      src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs
  16. 42 0
      src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs
  17. 7 0
      src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  18. 147 76
      src/Renci.SshNet/ClientAuthentication.cs
  19. 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();
         }
 

+ 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)

+ 15 - 1
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();
@@ -39,11 +46,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)

+ 14 - 0
src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs

@@ -0,0 +1,14 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit
+    {
+        [TestMethod]
+        public void Test()
+        {
+            Assert.Fail();
+        }
+    }
+}

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

@@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes
     [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();
@@ -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);
         }
     }
 }

+ 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
+    /// 
+    ///                    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-partial1");
+
+            /* 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);
+        }
+    }
+}

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

@@ -0,0 +1,144 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and  authentication methods, and partial
+    /// success limit is set to 2.
+    ///
+    /// Authentication proceeds as follows:
+    /// 
+    /// 1 x     * Client performs 'none' authentication attempt.
+    ///         * Server responds with 'failure', and 'password' allowed authentication method.
+    /// 
+    /// 1 x     * Client performs 'password' authentication attempt.
+    ///         * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods.
+    /// 
+    /// 1 x     * Client performs 'publickey' authentication attempt.
+    ///         * Server responds with 'failure'.
+    ///
+    /// 1 x     * Client performs 'password' authentication attempt.
+    ///         * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods.
+    /// 
+    /// 1 x     * Client performs 'keyboard-interactive' authentication attempt.
+    ///         * Server responds with 'failure'.
+    ///
+    /// Since the server only ever allowed the 'password' authentication method, there are no
+    /// authentication methods left to try after reaching the partial success limit for 'password'
+    /// and as such authentication fails.
+    /// </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);
+
+            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"});
+
+            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-failure");
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] {"keyboard-interactive"});
+
+            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");
+            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);
+        }
+    }
+}

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

@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and  authentication methods, and partial
+    /// success limit is set to 2.
+    ///
+    /// Authentication proceeds as follows:
+    /// 
+    /// 1 x     * Client performs 'none' authentication attempt.
+    ///         * Server responds with 'failure', and 'password' allowed authentication method.
+    /// 
+    /// 1 x     * Client performs 'password' authentication attempt.
+    ///         * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods.
+    /// 
+    /// 1 x     * Client performs 'publickey' authentication attempt.
+    ///         * Server responds with 'failure'.
+    ///
+    /// 1 x     * Client performs 'password' authentication attempt.
+    ///         * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods.
+    /// 
+    /// 1 x     * Client performs 'keyboard-interactive' authentication attempt.
+    ///         * Server responds with 'success'.
+    /// </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);
+
+            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"});
+
+            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-failure");
+
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.AllowedAuthentications)
+                                            .Returns(new[] { "keyboard-interactive" });
+
+            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");
+            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);
         }
     }
 }

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

@@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes
     [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();
@@ -20,34 +28,48 @@ namespace Renci.SshNet.Tests.Classes
 
             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"});
 
             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" });
+            PasswordAuthenticationMethodMock.InSequence(seq)
+                                            .Setup(p => p.Authenticate(SessionMock.Object))
+                                            .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.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.Success);
+            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);
         }
     }
 }

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

@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    /// <summary>
+    /// ConnectionInfo provides 'password' and 'publickey' authentication methods, and partial success limit is
+    /// set to 3.
+    ///
+    /// Authentication proceeds as follows:
+    /// 
+    /// 1 x     * Client performs 'none' authentication attempt.
+    ///         * Server responds with 'failure', and 'password' allowed authentication method.
+    /// 
+    /// 3 x     * Client performs 'password' authentication attempt.
+    ///         * Server responds with 'partial success', and 'password' allowed authentication method
+    /// 
+    /// Since the server only ever allowed the 'password' authentication method, there are no
+    /// authentication methods left to try after reaching the partial success limit for 'password'
+    /// and as such authentication fails.
+    /// </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);
+
+            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" });
+
+            for (var i = 0; i < _partialSuccessLimit; i++)
+            {
+                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"});
+            }
+
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey");
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            // used to construct exception message
+            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);
+        }
+    }
+}

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

@@ -139,11 +139,17 @@
     <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_Success_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_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 +269,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" />

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

@@ -34,6 +34,12 @@ namespace Renci.SshNet
             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)
@@ -72,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)
             {
@@ -86,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:
@@ -134,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>