Pārlūkot izejas kodu

Added ISession, IAuthenticationMethod and IConnectionInfo interfaces to allow mocking.
Extracted authentication logic into ClientAuthentication class to allow improve testabiity.
When a given authentication method has already been authenticated against, then skip the method if there are still alternative authentication methods.
Fixes issue #2399.
Added test for several authentication scenarios.

Part 2 to come where new interfaces will be added to other projects.

Gert Driesen 11 gadi atpakaļ
vecāks
revīzija
2427090ecb
21 mainītis faili ar 794 papildinājumiem un 101 dzēšanām
  1. 13 1
      Renci.SshClient/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj
  2. 26 1
      Renci.SshClient/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj
  3. 4 0
      Renci.SshClient/Renci.SshNet.Tests.NET35/packages.config
  4. 84 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs
  5. 70 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs
  6. 63 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs
  7. 55 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs
  8. 61 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs
  9. 55 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs
  10. 53 0
      Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs
  11. 11 0
      Renci.SshClient/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj
  12. 4 0
      Renci.SshClient/Renci.SshNet.Tests/packages.config
  13. 16 1
      Renci.SshClient/Renci.SshNet.WindowsPhone8/Renci.SshNet.WindowsPhone8.csproj
  14. 20 8
      Renci.SshClient/Renci.SshNet/AuthenticationMethod.cs
  15. 158 0
      Renci.SshClient/Renci.SshNet/ClientAuthentication.cs
  16. 14 83
      Renci.SshClient/Renci.SshNet/ConnectionInfo.cs
  17. 35 0
      Renci.SshClient/Renci.SshNet/IAuthenticationMethod.cs
  18. 28 0
      Renci.SshClient/Renci.SshNet/IConnectionInfo.cs
  19. 12 0
      Renci.SshClient/Renci.SshNet/ISession.cs
  20. 4 0
      Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj
  21. 8 7
      Renci.SshClient/Renci.SshNet/Session.cs

+ 13 - 1
Renci.SshClient/Renci.SshNet.NET35/Renci.SshNet.NET35.csproj

@@ -87,6 +87,9 @@
     <Compile Include="..\Renci.SshNet\CipherInfo.cs">
       <Link>CipherInfo.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\ClientAuthentication.cs">
+      <Link>ClientAuthentication.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\CommandAsyncResult.cs">
       <Link>CommandAsyncResult.cs</Link>
     </Compile>
@@ -276,6 +279,15 @@
     <Compile Include="..\Renci.SshNet\HashInfo.cs">
       <Link>HashInfo.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IAuthenticationMethod.cs">
+      <Link>IAuthenticationMethod.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\IConnectionInfo.cs">
+      <Link>IConnectionInfo.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\ISession.cs">
+      <Link>ISession.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\KeyboardInteractiveAuthenticationMethod.cs">
       <Link>KeyboardInteractiveAuthenticationMethod.cs</Link>
     </Compile>
@@ -895,7 +907,7 @@
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
+      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 26 - 1
Renci.SshClient/Renci.SshNet.Tests.NET35/Renci.SshNet.Tests.NET35.csproj

@@ -45,6 +45,9 @@
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+    <Reference Include="Moq">
+      <HintPath>..\packages\Moq.4.2.1409.1722\lib\net35\Moq.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core">
       <RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -79,6 +82,27 @@
     <Compile Include="..\Renci.SshNet.Tests\Classes\CipherInfoTest.cs">
       <Link>Classes\CipherInfoTest.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTestBase.cs">
+      <Link>Classes\ClientAuthenticationTestBase.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs">
+      <Link>Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs">
+      <Link>Classes\ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs">
+      <Link>Classes\ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs">
+      <Link>Classes\ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs">
+      <Link>Classes\ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet.Tests\Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs">
+      <Link>Classes\ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet.Tests\Classes\CommandAsyncResultTest.cs">
       <Link>Classes\CommandAsyncResultTest.cs</Link>
     </Compile>
@@ -774,6 +798,7 @@
     <None Include="..\Renci.SshNet.snk">
       <Link>Renci.SshNet.snk</Link>
     </None>
+    <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="..\Renci.SshNet.Tests\Data\Key.SSH2.DSA.Encrypted.Des.CBC.12345.txt">
@@ -792,7 +817,7 @@
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" />
+      <UserProperties ProjectLinkReference="c45379b9-17b1-4e89-bc2e-6d41726413e8" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 4 - 0
Renci.SshClient/Renci.SshNet.Tests.NET35/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Moq" version="4.2.1409.1722" targetFramework="net35" />
+</packages>

+ 84 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs

@@ -0,0 +1,84 @@
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Moq.Protected;
+using Renci.SshNet.Common;
+using Renci.SshNet.Tests.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public abstract class ClientAuthenticationTestBase : TestBase
+    {
+        internal Mock<IConnectionInfo> ConnectionInfoMock { get; private set; }
+        internal Mock<ISession> SessionMock { get; private set; }
+        internal Mock<IAuthenticationMethod> NoneAuthenticationMethodMock { get; private set; }
+        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 void CreateMocks()
+        {
+            ConnectionInfoMock = new Mock<IConnectionInfo>(MockBehavior.Strict);
+            SessionMock = new Mock<ISession>(MockBehavior.Strict);
+            NoneAuthenticationMethodMock = new Mock<IAuthenticationMethod>(MockBehavior.Strict);
+            PasswordAuthenticationMethodMock = new Mock<IAuthenticationMethod>(MockBehavior.Strict);
+            PublicKeyAuthenticationMethodMock = new Mock<IAuthenticationMethod>(MockBehavior.Strict);
+            KeyboardInteractiveAuthenticationMethodMock = new Mock<IAuthenticationMethod>(MockBehavior.Strict);
+        }
+
+        protected abstract void SetupMocks();
+
+        protected abstract void Act();
+
+        protected override void OnInit()
+        {
+            base.OnInit();
+
+            // Arrange
+            CreateMocks();
+            SetupMocks();
+            ClientAuthentication = new ClientAuthentication();
+
+            // Act
+            Act();
+        }
+
+        [TestMethod]
+        public void RegisterMessageWithUserAuthFailureShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE"), Times.Once);
+        }
+
+        [TestMethod]
+        public void RegisterMessageWithUserAuthSuccessShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS"), Times.Once);
+        }
+
+        [TestMethod]
+        public void RegisterMessageWithUserAuthBannerShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER"), Times.Once);
+        }
+
+        [TestMethod]
+        public void UnRegisterMessageWithUserAuthFailureShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE"), Times.Once);
+        }
+
+        [TestMethod]
+        public void UnRegisterMessageWithUserAuthSuccessShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS"), Times.Once);
+        }
+
+        [TestMethod]
+        public void UnRegisterMessageWithUserAuthBannerShouldBeInvokedOnce()
+        {
+            SessionMock.Verify(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER"), Times.Once);
+        }
+    }
+}

+ 70 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs

@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed : ClientAuthenticationTestBase
+    {
+        private SshAuthenticationException _actualException;
+
+        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" });
+
+            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);
+            // obtain name for inclusion in SshAuthenticationException
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            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 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("Permission denied (password).", _actualException.Message);
+        }
+    }
+}

+ 63 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs

@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotSupported : ClientAuthenticationTestBase
+    {
+        private SshAuthenticationException _actualException;
+
+        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
+                });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                .Setup(p => p.AllowedAuthentications)
+                .Returns(new[] { "password" });
+
+            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 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("No suitable authentication method found to complete authentication (password).", _actualException.Message);
+        }
+    }
+}

+ 55 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs

@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase
+    {
+        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>
+                {
+                    PasswordAuthenticationMethodMock.Object,
+                    PublicKeyAuthenticationMethodMock.Object,
+                    KeyboardInteractiveAuthenticationMethodMock.Object,
+                });
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password", "publickey" });
+            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");
+
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object))
+                .Returns(AuthenticationResult.PartialSuccess);
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications)
+                .Returns(new[] { "keyboard-interactive", "publickey" });
+            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);
+
+            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 Act()
+        {
+            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+    }
+}

+ 61 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs

@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase
+    {
+        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>
+                {
+                    PasswordAuthenticationMethodMock.Object,
+                    PublicKeyAuthenticationMethodMock.Object,
+                    KeyboardInteractiveAuthenticationMethodMock.Object,
+                });
+            NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password", "publickey" });
+            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");
+
+            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.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.PartialSuccess);
+            PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications)
+                .Returns(new[] { "password", "publickey", "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.Name).Returns("keyboard-interactive");
+            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 Act()
+        {
+            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+    }
+}

+ 55 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs

@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod : ClientAuthenticationTestBase
+    {
+        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>
+                {
+                    PasswordAuthenticationMethodMock.Object,
+                    PublicKeyAuthenticationMethodMock.Object,
+                });
+            NoneAuthenticationMethodMock.InSequence(seq)
+                .Setup(p => p.AllowedAuthentications)
+                .Returns(new[] { "publickey", "password" });
+
+            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.Failure);
+            // obtain name for inclusion in SshAuthenticationException
+            PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password");
+
+            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 Act()
+        {
+            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+    }
+}

+ 53 - 0
Renci.SshClient/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs

@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Renci.SshNet.Tests.Classes
+{
+    [TestClass]
+    public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase
+    {
+        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" });
+
+            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");
+
+            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 Act()
+        {
+            ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object);
+        }
+    }
+}

+ 11 - 0
Renci.SshClient/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj

@@ -49,6 +49,9 @@
   <ItemGroup>
     <Reference Include="Microsoft.CSharp" />
     <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+    <Reference Include="Moq">
+      <HintPath>..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core">
       <RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -63,6 +66,13 @@
     </CodeAnalysisDependentAssemblyPaths>
   </ItemGroup>
   <ItemGroup>
+    <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_DifferentAllowedAuthenticationsAfterPartialSuccess.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\Security\AlgorithmTest.cs" />
     <Compile Include="Classes\Security\CertificateHostAlgorithmTest.cs" />
     <Compile Include="Classes\Security\Cryptography\BlockCipherTest.cs" />
@@ -341,6 +351,7 @@
     <None Include="..\Renci.SshNet.snk">
       <Link>Renci.SshNet.snk</Link>
     </None>
+    <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Data\Key.SSH2.RSA.Encrypted.Des.CBC.12345.txt" />

+ 4 - 0
Renci.SshClient/Renci.SshNet.Tests/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Moq" version="4.2.1409.1722" targetFramework="net40" />
+</packages>

+ 16 - 1
Renci.SshClient/Renci.SshNet.WindowsPhone8/Renci.SshNet.WindowsPhone8.csproj

@@ -99,6 +99,9 @@
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="..\Renci.SshNet.Silverlight\Channels\ChannelDirectTcpip.SilverlightShared.cs">
+      <Link>ChannelDirectTcpip.SilverlightShared.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\AuthenticationMethod.cs">
       <Link>AuthenticationMethod.cs</Link>
     </Compile>
@@ -132,6 +135,9 @@
     <Compile Include="..\Renci.SshNet\CipherInfo.cs">
       <Link>CipherInfo.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\ClientAuthentication.cs">
+      <Link>ClientAuthentication.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\CommandAsyncResult.cs">
       <Link>CommandAsyncResult.cs</Link>
     </Compile>
@@ -282,6 +288,15 @@
     <Compile Include="..\Renci.SshNet\HashInfo.cs">
       <Link>HashInfo.cs</Link>
     </Compile>
+    <Compile Include="..\Renci.SshNet\IAuthenticationMethod.cs">
+      <Link>IAuthenticationMethod.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\IConnectionInfo.cs">
+      <Link>IConnectionInfo.cs</Link>
+    </Compile>
+    <Compile Include="..\Renci.SshNet\ISession.cs">
+      <Link>ISession.cs</Link>
+    </Compile>
     <Compile Include="..\Renci.SshNet\KeyboardInteractiveAuthenticationMethod.cs">
       <Link>KeyboardInteractiveAuthenticationMethod.cs</Link>
     </Compile>
@@ -889,7 +904,7 @@
   <Import Project="$(MSBuildExtensionsPath)\Microsoft\$(TargetFrameworkIdentifier)\$(TargetFrameworkVersion)\Microsoft.$(TargetFrameworkIdentifier).CSharp.targets" />
   <ProjectExtensions>
     <VisualStudio>
-      <UserProperties ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" />
+      <UserProperties ProjectLinkerExcludeFilter="\\?desktop(\\.*)?$;\\?silverlight(\\.*)?$;\.desktop;\.silverlight;\.xaml;^service references(\\.*)?$;\.clientconfig;^web references(\\.*)?$" ProjectLinkReference="2f5f8c90-0bd1-424f-997c-7bc6280919d1" />
     </VisualStudio>
   </ProjectExtensions>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 20 - 8
Renci.SshClient/Renci.SshNet/AuthenticationMethod.cs

@@ -7,11 +7,14 @@ namespace Renci.SshNet
     /// <summary>
     /// Base class for all supported authentication methods
     /// </summary>
-    public abstract class AuthenticationMethod
+    public abstract class AuthenticationMethod : IAuthenticationMethod
     {
         /// <summary>
-        /// Gets authentication method name
+        /// Gets the name of the authentication method.
         /// </summary>
+        /// <value>
+        /// The name of the authentication method.
+        /// </value>
         public abstract string Name { get; }
 
         /// <summary>
@@ -19,11 +22,6 @@ namespace Renci.SshNet
         /// </summary>
         public string Username { get; private set; }
 
-        /// <summary>
-        /// Gets the authentication error message.
-        /// </summary>
-        public string ErrorMessage { get; private set; }
-
         /// <summary>
         /// Gets list of allowed authentications.
         /// </summary>
@@ -46,7 +44,21 @@ namespace Renci.SshNet
         /// Authenticates the specified session.
         /// </summary>
         /// <param name="session">The session to authenticate.</param>
-        /// <returns>Result of authentication  process.</returns>
+        /// <returns>
+        /// The result of the authentication process.
+        /// </returns>
         public abstract AuthenticationResult Authenticate(Session session);
+
+        /// <summary>
+        /// Authenticates the specified session.
+        /// </summary>
+        /// <param name="session">The session to authenticate.</param>
+        /// <returns>
+        /// The result of the authentication process.
+        /// </returns>
+        AuthenticationResult IAuthenticationMethod.Authenticate(ISession session)
+        {
+            return Authenticate((Session) session);
+        }
     }
 }

+ 158 - 0
Renci.SshClient/Renci.SshNet/ClientAuthentication.cs

@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet
+{
+    internal class ClientAuthentication
+    {
+        public void Authenticate(IConnectionInfo connectionInfo, ISession session)
+        {
+            if (connectionInfo == null)
+                throw new ArgumentNullException("connectionInfo");
+            if (session == null)
+                throw new ArgumentNullException("session");
+
+            session.RegisterMessage("SSH_MSG_USERAUTH_FAILURE");
+            session.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
+            session.RegisterMessage("SSH_MSG_USERAUTH_BANNER");
+            session.UserAuthenticationBannerReceived += connectionInfo.UserAuthenticationBannerReceived;
+
+            try
+            {
+                // the exception to report an authentication failure with
+                SshAuthenticationException authenticationException = null;
+
+                // try to authenticate against none
+                var noneAuthenticationMethod = connectionInfo.CreateNoneAuthenticationMethod();
+
+                var authenticated = noneAuthenticationMethod.Authenticate(session);
+                if (authenticated != AuthenticationResult.Success)
+                {
+                    if (!TryAuthenticate(session, new AuthenticationState(connectionInfo.AuthenticationMethods.ToList()), noneAuthenticationMethod.AllowedAuthentications.ToList(), ref authenticationException))
+                    {
+                        throw authenticationException;
+                    }
+                }
+            }
+            finally
+            {
+                session.UserAuthenticationBannerReceived -= connectionInfo.UserAuthenticationBannerReceived;
+                session.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE");
+                session.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
+                session.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER");
+            }
+
+        }
+
+        private bool TryAuthenticate(ISession session,
+                                     AuthenticationState authenticationState,
+                                     ICollection<string> allowedAuthenticationMethods,
+                                     ref SshAuthenticationException authenticationException)
+        {
+            if (!allowedAuthenticationMethods.Any())
+            {
+                authenticationException = new SshAuthenticationException("No authentication methods defined on SSH server.");
+                return false;
+            }
+
+            // 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 = authenticationState.SupportedAuthenticationMethods.Where(a => allowedAuthenticationMethods.Contains(a.Name)).ToList();
+            if (!matchingAuthenticationMethods.Any())
+            {
+                authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", string.Join(",", allowedAuthenticationMethods.ToArray())));
+                return false;
+            }
+
+            for (var i = 0; i < matchingAuthenticationMethods.Count; i++)
+            {
+                var authenticationMethod = matchingAuthenticationMethods[i];
+
+                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
+                // methods after a partial success
+
+                if (authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod))
+                {
+                    var isLastAuthenticationMethod = i == (matchingAuthenticationMethods.Count - 1);
+                    if (!isLastAuthenticationMethod)
+                        continue;
+                }
+                else
+                {
+                    // update state to reflect previosuly executed authentication methods
+                    authenticationState.ExecutedAuthenticationMethods.Add(authenticationMethod);
+                }
+
+                var authenticationResult = authenticationMethod.Authenticate(session);
+                switch (authenticationResult)
+                {
+                    case AuthenticationResult.PartialSuccess:
+                        if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications.ToList(), ref authenticationException))
+                        {
+                            authenticationResult = AuthenticationResult.Success;
+                        }
+                        break;
+                    case AuthenticationResult.Failure:
+                        authenticationState.FailedAuthenticationMethods.Add(authenticationMethod);
+                        authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name));
+                        break;
+                    case AuthenticationResult.Success:
+                        authenticationException = null;
+                        break;
+                }
+
+                if (authenticationResult == AuthenticationResult.Success)
+                    return true;
+            }
+
+            return false;
+        }
+
+        private class AuthenticationState
+        {
+            private readonly IList<IAuthenticationMethod> _supportedAuthenticationMethods;
+
+            public AuthenticationState(IList<IAuthenticationMethod> supportedAuthenticationMethods)
+            {
+                _supportedAuthenticationMethods = supportedAuthenticationMethods;
+                ExecutedAuthenticationMethods = new List<IAuthenticationMethod>();
+                FailedAuthenticationMethods = new List<IAuthenticationMethod>();
+            }
+
+            /// <summary>
+            /// Gets the list of authentication methods that were previously executed.
+            /// </summary>
+            /// <value>
+            /// The list of authentication methods that were previously executed.
+            /// </value>
+            public IList<IAuthenticationMethod> ExecutedAuthenticationMethods { get; private set; }
+
+            /// <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; }
+
+            /// <summary>
+            /// Gets the list of supported authentication methods.
+            /// </summary>
+            /// <value>
+            /// The list of supported authentication methods.
+            /// </value>
+            public IEnumerable<IAuthenticationMethod> SupportedAuthenticationMethods
+            {
+                get { return _supportedAuthenticationMethods; }
+            }
+        }
+    }
+}

+ 14 - 83
Renci.SshClient/Renci.SshNet/ConnectionInfo.cs

@@ -20,7 +20,7 @@ namespace Renci.SshNet
     /// This class is NOT thread-safe. Do not use the same <see cref="ConnectionInfo"/> with multiple
     /// client instances.
     /// </remarks>
-    public class ConnectionInfo
+    public class ConnectionInfo : IConnectionInfo
     {
         internal static int DEFAULT_PORT = 22;
 
@@ -392,97 +392,28 @@ namespace Renci.SshNet
         /// <exception cref="SshAuthenticationException">No suitable authentication method found to complete authentication, or permission denied.</exception>
         public void Authenticate(Session session)
         {
-            if (session == null)
-                throw new ArgumentNullException("session");
-
-            session.RegisterMessage("SSH_MSG_USERAUTH_FAILURE");
-            session.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
-            session.RegisterMessage("SSH_MSG_USERAUTH_BANNER");
-            session.UserAuthenticationBannerReceived += Session_UserAuthenticationBannerReceived;
-
-            try
-            {
-                // the exception to report an authentication failure with
-                SshAuthenticationException authenticationException = null;
-
-                // try to authenticate against none
-                var noneAuthenticationMethod = new NoneAuthenticationMethod(this.Username);
-
-                var authenticated = noneAuthenticationMethod.Authenticate(session);
-                if (authenticated != AuthenticationResult.Success)
-                {
-                    var failedAuthenticationMethods = new List<AuthenticationMethod>();
-                    if (TryAuthenticate(session, noneAuthenticationMethod.AllowedAuthentications.ToList(), failedAuthenticationMethods, ref authenticationException))
-                    {
-                        authenticated = AuthenticationResult.Success;
-                    }
-                }
-
-                this.IsAuthenticated = authenticated == AuthenticationResult.Success;
-                if (!IsAuthenticated)
-                    throw authenticationException;
-            }
-            finally
-            {
-                session.UserAuthenticationBannerReceived -= Session_UserAuthenticationBannerReceived;
-                session.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE");
-                session.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
-                session.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER");
-            }
+            var clientAuthentication = new ClientAuthentication();
+            clientAuthentication.Authenticate(this, session);
         }
 
-        private bool TryAuthenticate(Session session, IList<string> allowedAuthenticationMethods, IList<AuthenticationMethod> failedAuthenticationMethods, ref SshAuthenticationException authenticationException)
+        void IConnectionInfo.UserAuthenticationBannerReceived(object sender, MessageEventArgs<BannerMessage> e)
         {
-            if (!allowedAuthenticationMethods.Any())
+            var authenticationBanner = AuthenticationBanner;
+            if (authenticationBanner != null)
             {
-                authenticationException = new SshAuthenticationException("No authentication methods defined on SSH server.");
-                return false;
-            }
-
-            // 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 = AuthenticationMethods.Where(a => allowedAuthenticationMethods.Contains(a.Name)).ToList();
-            if (!matchingAuthenticationMethods.Any())
-            {
-                authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", string.Join(",", allowedAuthenticationMethods.ToArray())));
-                return false;
-            }
-
-            foreach (var authenticationMethod in matchingAuthenticationMethods)
-            {
-                if (failedAuthenticationMethods.Contains(authenticationMethod))
-                    continue;
-
-                var authenticationResult = authenticationMethod.Authenticate(session);
-                switch (authenticationResult)
-                {
-                    case AuthenticationResult.PartialSuccess:
-                        if (TryAuthenticate(session, authenticationMethod.AllowedAuthentications.ToList(), failedAuthenticationMethods, ref authenticationException))
-                            authenticationResult = AuthenticationResult.Success;
-                        break;
-                    case AuthenticationResult.Failure:
-                        failedAuthenticationMethods.Add(authenticationMethod);
-                        authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name));
-                        break;
-                    case AuthenticationResult.Success:
-                        authenticationException = null;
-                        break;
-                }
-
-                if (authenticationResult == AuthenticationResult.Success)
-                    return true;
+                authenticationBanner(this,
+                    new AuthenticationBannerEventArgs(Username, e.Message.Message, e.Message.Language));
             }
+        }
 
-            return false;
+        IAuthenticationMethod IConnectionInfo.CreateNoneAuthenticationMethod()
+        {
+            return new NoneAuthenticationMethod(Username);
         }
 
-        private void Session_UserAuthenticationBannerReceived(object sender, MessageEventArgs<BannerMessage> e)
+        IEnumerable<IAuthenticationMethod> IConnectionInfo.AuthenticationMethods
         {
-            if (this.AuthenticationBanner != null)
-            {
-                this.AuthenticationBanner(this, new AuthenticationBannerEventArgs(this.Username, e.Message.Message, e.Message.Language));
-            }
+            get { return AuthenticationMethods.Cast<IAuthenticationMethod>(); }
         }
     }
 }

+ 35 - 0
Renci.SshClient/Renci.SshNet/IAuthenticationMethod.cs

@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Base interface for authentication of a session using a given method.
+    /// </summary>
+    internal interface IAuthenticationMethod
+    {
+        /// <summary>
+        /// Authenticates the specified session.
+        /// </summary>
+        /// <param name="session">The session to authenticate.</param>
+        /// <returns>
+        /// The result of the authentication process.
+        /// </returns>
+        AuthenticationResult Authenticate(ISession session);
+
+        /// <summary>
+        /// Gets the list of allowed authentications.
+        /// </summary>
+        /// <value>
+        /// The list of allowed authentications.
+        /// </value>
+        IEnumerable<string> AllowedAuthentications { get; }
+
+        /// <summary>
+        /// Gets the name of the authentication method.
+        /// </summary>
+        /// <value>
+        /// The name of the authentication method.
+        /// </value>
+        string Name { get; }
+    }
+}

+ 28 - 0
Renci.SshClient/Renci.SshNet/IConnectionInfo.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using Renci.SshNet.Messages.Authentication;
+
+namespace Renci.SshNet
+{
+    internal interface IConnectionInfo
+    {
+        /// <summary>
+        /// Gets the supported authentication methods for this connection.
+        /// </summary>
+        /// <value>
+        /// The supported authentication methods for this connection.
+        /// </value>
+        IEnumerable<IAuthenticationMethod> AuthenticationMethods { get; }
+
+        void UserAuthenticationBannerReceived(object sender, MessageEventArgs<BannerMessage> e);
+
+        /// <summary>
+        /// Creates a <see cref="NoneAuthenticationMethod"/> for the credentials represented
+        /// by the current <see cref="IConnectionInfo"/>.
+        /// </summary>
+        /// <returns>
+        /// A <see cref="NoneAuthenticationMethod"/> for the credentials represented by the
+        /// current <see cref="IConnectionInfo"/>.
+        /// </returns>
+        IAuthenticationMethod CreateNoneAuthenticationMethod();
+    }
+}

+ 12 - 0
Renci.SshClient/Renci.SshNet/ISession.cs

@@ -0,0 +1,12 @@
+using System;
+using Renci.SshNet.Messages.Authentication;
+
+namespace Renci.SshNet
+{
+    public interface ISession
+    {
+        void RegisterMessage(string messageName);
+        void UnRegisterMessage(string messageName);
+        event EventHandler<MessageEventArgs<BannerMessage>> UserAuthenticationBannerReceived;
+    }
+}

+ 4 - 0
Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj

@@ -75,6 +75,7 @@
     <Compile Include="Common\AuthenticationPasswordChangeEventArgs.cs" />
     <Compile Include="Common\AuthenticationPrompt.cs" />
     <Compile Include="Common\AuthenticationPromptEventArgs.cs" />
+    <Compile Include="ClientAuthentication.cs" />
     <Compile Include="Common\BigInteger.cs">
       <SubType>Code</SubType>
     </Compile>
@@ -147,6 +148,9 @@
     <Compile Include="HashInfo.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="IAuthenticationMethod.cs" />
+    <Compile Include="IConnectionInfo.cs" />
+    <Compile Include="ISession.cs" />
     <Compile Include="Messages\Transport\KeyExchangeEcdhInitMessage.cs" />
     <Compile Include="Messages\Transport\KeyExchangeEcdhReplyMessage.cs" />
     <Compile Include="Security\Cryptography\Hashes\RIPEMD160Hash.cs" />

+ 8 - 7
Renci.SshClient/Renci.SshNet/Session.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
+using System.Runtime.CompilerServices;
 using System.Security.Cryptography;
 using System.Text;
 using System.Text.RegularExpressions;
@@ -23,7 +24,7 @@ namespace Renci.SshNet
     /// <summary>
     /// Provides functionality to connect and interact with SSH server.
     /// </summary>
-    public partial class Session : IDisposable
+    public partial class Session : IDisposable, ISession
     {
         /// <summary>
         /// Specifies maximum packet size defined by the protocol.
@@ -294,7 +295,7 @@ namespace Renci.SshNet
         public event EventHandler<ExceptionEventArgs> ErrorOccured;
 
         /// <summary>
-        /// Occurs when session has been disconnected form the server.
+        /// Occurs when session has been disconnected from the server.
         /// </summary>
         public event EventHandler<EventArgs> Disconnected;
 
@@ -303,6 +304,11 @@ namespace Renci.SshNet
         /// </summary>
         public event EventHandler<HostKeyEventArgs> HostKeyReceived;
 
+        /// <summary>
+        /// Occurs when <see cref="BannerMessage"/> message is received from the server.
+        /// </summary>
+        public event EventHandler<MessageEventArgs<BannerMessage>> UserAuthenticationBannerReceived;
+
         #region Message events
 
         /// <summary>
@@ -360,11 +366,6 @@ namespace Renci.SshNet
         /// </summary>
         internal event EventHandler<MessageEventArgs<SuccessMessage>> UserAuthenticationSuccessReceived;
 
-        /// <summary>
-        /// Occurs when <see cref="BannerMessage"/> message received
-        /// </summary>
-        internal event EventHandler<MessageEventArgs<BannerMessage>> UserAuthenticationBannerReceived;
-
         /// <summary>
         /// Occurs when <see cref="GlobalRequestMessage"/> message received
         /// </summary>