ClientAuthentication.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using System;
  2. using System.Collections.Generic;
  3. using Renci.SshNet.Common;
  4. namespace Renci.SshNet
  5. {
  6. internal class ClientAuthentication : IClientAuthentication
  7. {
  8. private readonly int _partialSuccessLimit;
  9. /// <summary>
  10. /// Initializes a new <see cref="ClientAuthentication"/> instance.
  11. /// </summary>
  12. /// <param name="partialSuccessLimit">The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.</param>
  13. /// <exception cref="ArgumentOutOfRangeException"><paramref name="partialSuccessLimit"/> is less than one.</exception>
  14. public ClientAuthentication(int partialSuccessLimit)
  15. {
  16. if (partialSuccessLimit < 1)
  17. throw new ArgumentOutOfRangeException("partialSuccessLimit", "Cannot be less than one.");
  18. _partialSuccessLimit = partialSuccessLimit;
  19. }
  20. /// <summary>
  21. /// Gets the number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can
  22. /// result in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
  23. /// </summary>
  24. /// <value>
  25. /// The number of times an authentication attempt with any given <see cref="IAuthenticationMethod"/> can result
  26. /// in <see cref="AuthenticationResult.PartialSuccess"/> before it is disregarded.
  27. /// </value>
  28. internal int PartialSuccessLimit
  29. {
  30. get { return _partialSuccessLimit; }
  31. }
  32. /// <summary>
  33. /// Attempts to authentication for a given <see cref="ISession"/> using the <see cref="IConnectionInfoInternal.AuthenticationMethods"/>
  34. /// of the specified <see cref="IConnectionInfoInternal"/>.
  35. /// </summary>
  36. /// <param name="connectionInfo">A <see cref="IConnectionInfoInternal"/> to use for authenticating.</param>
  37. /// <param name="session">The <see cref="ISession"/> for which to perform authentication.</param>
  38. public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session)
  39. {
  40. if (connectionInfo == null)
  41. throw new ArgumentNullException("connectionInfo");
  42. if (session == null)
  43. throw new ArgumentNullException("session");
  44. session.RegisterMessage("SSH_MSG_USERAUTH_FAILURE");
  45. session.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
  46. session.RegisterMessage("SSH_MSG_USERAUTH_BANNER");
  47. session.UserAuthenticationBannerReceived += connectionInfo.UserAuthenticationBannerReceived;
  48. try
  49. {
  50. // the exception to report an authentication failure with
  51. SshAuthenticationException authenticationException = null;
  52. // try to authenticate against none
  53. var noneAuthenticationMethod = connectionInfo.CreateNoneAuthenticationMethod();
  54. var authenticated = noneAuthenticationMethod.Authenticate(session);
  55. if (authenticated != AuthenticationResult.Success)
  56. {
  57. if (!TryAuthenticate(session, new AuthenticationState(connectionInfo.AuthenticationMethods), noneAuthenticationMethod.AllowedAuthentications, ref authenticationException))
  58. {
  59. throw authenticationException;
  60. }
  61. }
  62. }
  63. finally
  64. {
  65. session.UserAuthenticationBannerReceived -= connectionInfo.UserAuthenticationBannerReceived;
  66. session.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE");
  67. session.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS");
  68. session.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER");
  69. }
  70. }
  71. private bool TryAuthenticate(ISession session,
  72. AuthenticationState authenticationState,
  73. string[] allowedAuthenticationMethods,
  74. ref SshAuthenticationException authenticationException)
  75. {
  76. if (allowedAuthenticationMethods.Length == 0)
  77. {
  78. authenticationException = new SshAuthenticationException("No authentication methods defined on SSH server.");
  79. return false;
  80. }
  81. // we want to try authentication methods in the order in which they were
  82. // passed in the ctor, not the order in which the SSH server returns
  83. // the allowed authentication methods
  84. var matchingAuthenticationMethods = authenticationState.GetSupportedAuthenticationMethods(allowedAuthenticationMethods);
  85. if (matchingAuthenticationMethods.Count == 0)
  86. {
  87. authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).",
  88. string.Join(",", allowedAuthenticationMethods)));
  89. return false;
  90. }
  91. foreach (var authenticationMethod in authenticationState.GetActiveAuthenticationMethods(matchingAuthenticationMethods))
  92. {
  93. // guard against a stack overlow for servers that do not update the list of allowed authentication
  94. // methods after a partial success
  95. if (authenticationState.GetPartialSuccessCount(authenticationMethod) >= _partialSuccessLimit)
  96. {
  97. // TODO Get list of all authentication methods that have reached the partial success limit?
  98. authenticationException = new SshAuthenticationException(string.Format("Reached authentication attempt limit for method ({0}).",
  99. authenticationMethod.Name));
  100. continue;
  101. }
  102. var authenticationResult = authenticationMethod.Authenticate(session);
  103. switch (authenticationResult)
  104. {
  105. case AuthenticationResult.PartialSuccess:
  106. authenticationState.RecordPartialSuccess(authenticationMethod);
  107. if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications, ref authenticationException))
  108. {
  109. authenticationResult = AuthenticationResult.Success;
  110. }
  111. break;
  112. case AuthenticationResult.Failure:
  113. authenticationState.RecordFailure(authenticationMethod);
  114. authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name));
  115. break;
  116. case AuthenticationResult.Success:
  117. authenticationException = null;
  118. break;
  119. }
  120. if (authenticationResult == AuthenticationResult.Success)
  121. return true;
  122. }
  123. return false;
  124. }
  125. private class AuthenticationState
  126. {
  127. private readonly IList<IAuthenticationMethod> _supportedAuthenticationMethods;
  128. /// <summary>
  129. /// Records if a given <see cref="IAuthenticationMethod"/> has been tried, and how many times this resulted
  130. /// in <see cref="AuthenticationResult.PartialSuccess"/>.
  131. /// </summary>
  132. /// <remarks>
  133. /// When there's no entry for a given <see cref="IAuthenticationMethod"/>, then it was never tried.
  134. /// </remarks>
  135. private readonly Dictionary<IAuthenticationMethod, int> _authenticationMethodPartialSuccessRegister;
  136. /// <summary>
  137. /// Holds the list of authentications methods that failed.
  138. /// </summary>
  139. private readonly List<IAuthenticationMethod> _failedAuthenticationMethods;
  140. public AuthenticationState(IList<IAuthenticationMethod> supportedAuthenticationMethods)
  141. {
  142. _supportedAuthenticationMethods = supportedAuthenticationMethods;
  143. _failedAuthenticationMethods = new List<IAuthenticationMethod>();
  144. _authenticationMethodPartialSuccessRegister = new Dictionary<IAuthenticationMethod, int>();
  145. }
  146. /// <summary>
  147. /// Records a <see cref="AuthenticationResult.Failure"/> authentication attempt for the specified
  148. /// <see cref="IAuthenticationMethod"/> .
  149. /// </summary>
  150. /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
  151. public void RecordFailure(IAuthenticationMethod authenticationMethod)
  152. {
  153. _failedAuthenticationMethods.Add(authenticationMethod);
  154. }
  155. /// <summary>
  156. /// Records a <see cref="AuthenticationResult.PartialSuccess"/> authentication attempt for the specified
  157. /// <see cref="IAuthenticationMethod"/> .
  158. /// </summary>
  159. /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/> for which to record the result of an authentication attempt.</param>
  160. public void RecordPartialSuccess(IAuthenticationMethod authenticationMethod)
  161. {
  162. int partialSuccessCount;
  163. if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount))
  164. {
  165. _authenticationMethodPartialSuccessRegister[authenticationMethod] = ++partialSuccessCount;
  166. }
  167. else
  168. {
  169. _authenticationMethodPartialSuccessRegister.Add(authenticationMethod, 1);
  170. }
  171. }
  172. /// <summary>
  173. /// Returns the number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
  174. /// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
  175. /// </summary>
  176. /// <param name="authenticationMethod">An <see cref="IAuthenticationMethod"/>.</param>
  177. /// <returns>
  178. /// The number of times an authentication attempt with the specified <see cref="IAuthenticationMethod"/>
  179. /// has resulted in <see cref="AuthenticationResult.PartialSuccess"/>.
  180. /// </returns>
  181. public int GetPartialSuccessCount(IAuthenticationMethod authenticationMethod)
  182. {
  183. int partialSuccessCount;
  184. if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount))
  185. {
  186. return partialSuccessCount;
  187. }
  188. return 0;
  189. }
  190. /// <summary>
  191. /// Returns a list of supported authentication methods that match one of the specified allowed authentication
  192. /// methods.
  193. /// </summary>
  194. /// <param name="allowedAuthenticationMethods">A list of allowed authentication methods.</param>
  195. /// <returns>
  196. /// A list of supported authentication methods that match one of the specified allowed authentication methods.
  197. /// </returns>
  198. /// <remarks>
  199. /// The authentication methods are returned in the order in which they were specified in the list that was
  200. /// used to initialize the current <see cref="AuthenticationState"/> instance.
  201. /// </remarks>
  202. public List<IAuthenticationMethod> GetSupportedAuthenticationMethods(string[] allowedAuthenticationMethods)
  203. {
  204. var result = new List<IAuthenticationMethod>();
  205. foreach (var supportedAuthenticationMethod in _supportedAuthenticationMethods)
  206. {
  207. var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name;
  208. for (var i = 0; i < allowedAuthenticationMethods.Length; i++)
  209. {
  210. if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod)
  211. {
  212. result.Add(supportedAuthenticationMethod);
  213. break;
  214. }
  215. }
  216. }
  217. return result;
  218. }
  219. /// <summary>
  220. /// Returns the authentication methods from the specified list that have not yet failed.
  221. /// </summary>
  222. /// <param name="matchingAuthenticationMethods">A list of authentication methods.</param>
  223. /// <returns>
  224. /// The authentication methods from <paramref name="matchingAuthenticationMethods"/> that have not yet failed.
  225. /// </returns>
  226. /// <remarks>
  227. /// <para>
  228. /// This method first returns the authentication methods that have not yet been executed, and only then
  229. /// returns those for which an authentication attempt resulted in a <see cref="AuthenticationResult.PartialSuccess"/>.
  230. /// </para>
  231. /// <para>
  232. /// Any <see cref="IAuthenticationMethod"/> that has failed is skipped.
  233. /// </para>
  234. /// </remarks>
  235. public IEnumerable<IAuthenticationMethod> GetActiveAuthenticationMethods(List<IAuthenticationMethod> matchingAuthenticationMethods)
  236. {
  237. var skippedAuthenticationMethods = new List<IAuthenticationMethod>();
  238. for (var i = 0; i < matchingAuthenticationMethods.Count; i++)
  239. {
  240. var authenticationMethod = matchingAuthenticationMethods[i];
  241. // skip authentication methods that have already failed
  242. if (_failedAuthenticationMethods.Contains(authenticationMethod))
  243. continue;
  244. // delay use of authentication methods that had a PartialSuccess result
  245. if (_authenticationMethodPartialSuccessRegister.ContainsKey(authenticationMethod))
  246. {
  247. skippedAuthenticationMethods.Add(authenticationMethod);
  248. continue;
  249. }
  250. yield return authenticationMethod;
  251. }
  252. foreach (var authenticationMethod in skippedAuthenticationMethods)
  253. yield return authenticationMethod;
  254. }
  255. }
  256. }
  257. }