/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs

https://github.com/aspnet/AspNetCore · C# · 430 lines · 315 code · 56 blank · 59 comment · 59 complexity · 7c17478b8ebb1754de1027403414423e MD5 · raw file

  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Runtime.InteropServices;
  8. using System.Security.Claims;
  9. using System.Security.Principal;
  10. using System.Text.Encodings.Web;
  11. using System.Threading.Tasks;
  12. using Microsoft.AspNetCore.Connections.Features;
  13. using Microsoft.AspNetCore.Http;
  14. using Microsoft.Extensions.Logging;
  15. using Microsoft.Extensions.Options;
  16. using Microsoft.Extensions.Primitives;
  17. using Microsoft.Net.Http.Headers;
  18. namespace Microsoft.AspNetCore.Authentication.Negotiate
  19. {
  20. /// <summary>
  21. /// Authenticates requests using Negotiate, Kerberos, or NTLM.
  22. /// </summary>
  23. public class NegotiateHandler : AuthenticationHandler<NegotiateOptions>, IAuthenticationRequestHandler
  24. {
  25. private const string AuthPersistenceKey = nameof(AuthPersistence);
  26. private const string NegotiateVerb = "Negotiate";
  27. private const string AuthHeaderPrefix = NegotiateVerb + " ";
  28. private bool _requestProcessed;
  29. private INegotiateState _negotiateState;
  30. /// <summary>
  31. /// Creates a new <see cref="NegotiateHandler"/>
  32. /// </summary>
  33. /// <param name="options"></param>
  34. /// <param name="logger"></param>
  35. /// <param name="encoder"></param>
  36. /// <param name="clock"></param>
  37. public NegotiateHandler(IOptionsMonitor<NegotiateOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
  38. : base(options, logger, encoder, clock)
  39. { }
  40. /// <summary>
  41. /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
  42. /// If it is not provided a default instance is supplied which does nothing when the methods are called.
  43. /// </summary>
  44. protected new NegotiateEvents Events
  45. {
  46. get => (NegotiateEvents)base.Events;
  47. set => base.Events = value;
  48. }
  49. /// <summary>
  50. /// Creates the default events type.
  51. /// </summary>
  52. /// <returns></returns>
  53. protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new NegotiateEvents());
  54. private bool IsSupportedProtocol => HttpProtocol.IsHttp11(Request.Protocol) || HttpProtocol.IsHttp10(Request.Protocol);
  55. /// <summary>
  56. /// Intercepts incomplete Negotiate authentication handshakes and continues or completes them.
  57. /// </summary>
  58. /// <returns>True if a response was generated, false otherwise.</returns>
  59. public async Task<bool> HandleRequestAsync()
  60. {
  61. AuthPersistence persistence = null;
  62. bool authFailedEventCalled = false;
  63. try
  64. {
  65. if (_requestProcessed || Options.DeferToServer)
  66. {
  67. // This request was already processed but something is re-executing it like an exception handler.
  68. // Don't re-run because we could corrupt the connection state, e.g. if this was a stage2 NTLM request
  69. // that we've already completed the handshake for.
  70. // Or we're in deferral mode where we let the server handle the authentication.
  71. return false;
  72. }
  73. _requestProcessed = true;
  74. if (!IsSupportedProtocol)
  75. {
  76. // HTTP/1.0 and HTTP/1.1 are supported. Do not throw because this may be running on a server that supports
  77. // additional protocols.
  78. return false;
  79. }
  80. var connectionItems = GetConnectionItems();
  81. persistence = (AuthPersistence)connectionItems[AuthPersistenceKey];
  82. _negotiateState = persistence?.State;
  83. var authorizationHeader = Request.Headers[HeaderNames.Authorization];
  84. if (StringValues.IsNullOrEmpty(authorizationHeader))
  85. {
  86. if (_negotiateState?.IsCompleted == false)
  87. {
  88. throw new InvalidOperationException("An anonymous request was received in between authentication handshake requests.");
  89. }
  90. return false;
  91. }
  92. var authorization = authorizationHeader.ToString();
  93. string token = null;
  94. if (authorization.StartsWith(AuthHeaderPrefix, StringComparison.OrdinalIgnoreCase))
  95. {
  96. token = authorization.Substring(AuthHeaderPrefix.Length).Trim();
  97. }
  98. else
  99. {
  100. if (_negotiateState?.IsCompleted == false)
  101. {
  102. throw new InvalidOperationException("Non-negotiate request was received in between authentication handshake requests.");
  103. }
  104. return false;
  105. }
  106. // WinHttpHandler re-authenticates an existing connection if it gets another challenge on subsequent requests.
  107. if (_negotiateState?.IsCompleted == true)
  108. {
  109. Logger.Reauthenticating();
  110. _negotiateState.Dispose();
  111. _negotiateState = null;
  112. persistence.State = null;
  113. }
  114. _negotiateState ??= Options.StateFactory.CreateInstance();
  115. var outgoing = _negotiateState.GetOutgoingBlob(token, out var errorType, out var exception);
  116. if (errorType != BlobErrorType.None)
  117. {
  118. Logger.NegotiateError(errorType.ToString());
  119. _negotiateState.Dispose();
  120. _negotiateState = null;
  121. if (persistence?.State != null)
  122. {
  123. persistence.State.Dispose();
  124. persistence.State = null;
  125. }
  126. if (errorType == BlobErrorType.CredentialError)
  127. {
  128. Logger.CredentialError(exception);
  129. authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event.
  130. var result = await InvokeAuthenticateFailedEvent(exception);
  131. return result ?? false; // Default to skipping the handler, let AuthZ generate a new 401
  132. }
  133. else if (errorType == BlobErrorType.ClientError)
  134. {
  135. Logger.ClientError(exception);
  136. authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event.
  137. var result = await InvokeAuthenticateFailedEvent(exception);
  138. if (result.HasValue)
  139. {
  140. return result.Value;
  141. }
  142. Context.Response.StatusCode = StatusCodes.Status400BadRequest;
  143. return true; // Default to terminating request
  144. }
  145. throw exception;
  146. }
  147. if (!_negotiateState.IsCompleted)
  148. {
  149. persistence ??= EstablishConnectionPersistence(connectionItems);
  150. // Save the state long enough to complete the multi-stage handshake.
  151. // We'll remove it once complete if !PersistNtlm/KerberosCredentials.
  152. persistence.State = _negotiateState;
  153. Logger.IncompleteNegotiateChallenge();
  154. Response.StatusCode = StatusCodes.Status401Unauthorized;
  155. Response.Headers.Append(HeaderNames.WWWAuthenticate, AuthHeaderPrefix + outgoing);
  156. return true;
  157. }
  158. Logger.NegotiateComplete();
  159. // There can be a final blob of data we need to send to the client, but let the request execute as normal.
  160. if (!string.IsNullOrEmpty(outgoing))
  161. {
  162. Response.OnStarting(() =>
  163. {
  164. // Only include it if the response ultimately succeeds. This avoids adding it twice if Challenge is called again.
  165. if (Response.StatusCode < StatusCodes.Status400BadRequest)
  166. {
  167. Response.Headers.Append(HeaderNames.WWWAuthenticate, AuthHeaderPrefix + outgoing);
  168. }
  169. return Task.CompletedTask;
  170. });
  171. }
  172. // Deal with connection credential persistence.
  173. if (_negotiateState.Protocol == "NTLM" && !Options.PersistNtlmCredentials)
  174. {
  175. // NTLM was already put in the persitence cache on the prior request so we could complete the handshake.
  176. // Take it out if we don't want it to persist.
  177. Debug.Assert(object.ReferenceEquals(persistence?.State, _negotiateState),
  178. "NTLM is a two stage process, it must have already been in the cache for the handshake to succeed.");
  179. Logger.DisablingCredentialPersistence(_negotiateState.Protocol);
  180. persistence.State = null;
  181. Response.RegisterForDispose(_negotiateState);
  182. }
  183. else if (_negotiateState.Protocol == "Kerberos")
  184. {
  185. // Kerberos can require one or two stage handshakes
  186. if (Options.PersistKerberosCredentials)
  187. {
  188. Logger.EnablingCredentialPersistence();
  189. persistence ??= EstablishConnectionPersistence(connectionItems);
  190. persistence.State = _negotiateState;
  191. }
  192. else
  193. {
  194. if (persistence?.State != null)
  195. {
  196. Logger.DisablingCredentialPersistence(_negotiateState.Protocol);
  197. persistence.State = null;
  198. }
  199. Response.RegisterForDispose(_negotiateState);
  200. }
  201. }
  202. // Note we run the Authenticated event in HandleAuthenticateAsync so it is per-request rather than per connection.
  203. }
  204. catch (Exception ex)
  205. {
  206. if (authFailedEventCalled)
  207. {
  208. throw;
  209. }
  210. Logger.ExceptionProcessingAuth(ex);
  211. // Clear state so it's possible to retry on the same connection.
  212. _negotiateState?.Dispose();
  213. _negotiateState = null;
  214. if (persistence?.State != null)
  215. {
  216. persistence.State.Dispose();
  217. persistence.State = null;
  218. }
  219. var result = await InvokeAuthenticateFailedEvent(ex);
  220. if (result.HasValue)
  221. {
  222. return result.Value;
  223. }
  224. throw;
  225. }
  226. return false;
  227. }
  228. private async Task<bool?> InvokeAuthenticateFailedEvent(Exception ex)
  229. {
  230. var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex };
  231. await Events.AuthenticationFailed(errorContext);
  232. if (errorContext.Result != null)
  233. {
  234. if (errorContext.Result.Handled)
  235. {
  236. return true;
  237. }
  238. else if (errorContext.Result.Skipped)
  239. {
  240. return false;
  241. }
  242. else if (errorContext.Result.Failure != null)
  243. {
  244. throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure);
  245. }
  246. }
  247. return null;
  248. }
  249. /// <summary>
  250. /// Checks if the current request is authenticated and returns the user.
  251. /// </summary>
  252. /// <returns></returns>
  253. protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
  254. {
  255. if (!_requestProcessed)
  256. {
  257. throw new InvalidOperationException("AuthenticateAsync must not be called before the UseAuthentication middleware runs.");
  258. }
  259. if (!IsSupportedProtocol)
  260. {
  261. // Not supported. We don't throw because Negotiate may be set as the default auth
  262. // handler on a server that's running HTTP/1 and HTTP/2. We'll challenge HTTP/2 requests
  263. // that require auth and they'll downgrade to HTTP/1.1.
  264. return AuthenticateResult.NoResult();
  265. }
  266. if (_negotiateState == null)
  267. {
  268. return AuthenticateResult.NoResult();
  269. }
  270. if (!_negotiateState.IsCompleted)
  271. {
  272. // This case should have been rejected by HandleRequestAsync
  273. throw new InvalidOperationException("Attempting to use an incomplete authentication context.");
  274. }
  275. // Make a new copy of the user for each request, they are mutable objects and
  276. // things like ClaimsTransformation run per request.
  277. var identity = _negotiateState.GetIdentity();
  278. ClaimsPrincipal user;
  279. if (OperatingSystem.IsWindows() && identity is WindowsIdentity winIdentity)
  280. {
  281. user = new WindowsPrincipal(winIdentity);
  282. Response.RegisterForDispose(winIdentity);
  283. }
  284. else
  285. {
  286. user = new ClaimsPrincipal(new ClaimsIdentity(identity));
  287. }
  288. AuthenticatedContext authenticatedContext;
  289. if (Options.LdapSettings.EnableLdapClaimResolution)
  290. {
  291. var ldapContext = new LdapContext(Context, Scheme, Options, Options.LdapSettings)
  292. {
  293. Principal = user
  294. };
  295. await Events.RetrieveLdapClaims(ldapContext);
  296. if (ldapContext.Result != null)
  297. {
  298. return ldapContext.Result;
  299. }
  300. await LdapAdapter.RetrieveClaimsAsync(ldapContext.LdapSettings, ldapContext.Principal.Identity as ClaimsIdentity, Logger);
  301. authenticatedContext = new AuthenticatedContext(Context, Scheme, Options)
  302. {
  303. Principal = ldapContext.Principal
  304. };
  305. }
  306. else
  307. {
  308. authenticatedContext = new AuthenticatedContext(Context, Scheme, Options)
  309. {
  310. Principal = user
  311. };
  312. }
  313. await Events.Authenticated(authenticatedContext);
  314. if (authenticatedContext.Result != null)
  315. {
  316. return authenticatedContext.Result;
  317. }
  318. var ticket = new AuthenticationTicket(authenticatedContext.Principal, authenticatedContext.Properties, Scheme.Name);
  319. return AuthenticateResult.Success(ticket);
  320. }
  321. /// <summary>
  322. /// Issues a 401 WWW-Authenticate Negotiate challenge.
  323. /// </summary>
  324. /// <param name="properties"></param>
  325. /// <returns></returns>
  326. protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
  327. {
  328. // We allow issuing a challenge from an HTTP/2 request. Browser clients will gracefully downgrade to HTTP/1.1.
  329. // SocketHttpHandler will not downgrade (https://github.com/dotnet/corefx/issues/35195), but WinHttpHandler will.
  330. var eventContext = new ChallengeContext(Context, Scheme, Options, properties);
  331. await Events.Challenge(eventContext);
  332. if (eventContext.Handled)
  333. {
  334. return;
  335. }
  336. Response.StatusCode = StatusCodes.Status401Unauthorized;
  337. Response.Headers.Append(HeaderNames.WWWAuthenticate, NegotiateVerb);
  338. Logger.ChallengeNegotiate();
  339. }
  340. private AuthPersistence EstablishConnectionPersistence(IDictionary<object, object> items)
  341. {
  342. Debug.Assert(!items.ContainsKey(AuthPersistenceKey), "This should only be registered once per connection");
  343. var persistence = new AuthPersistence();
  344. RegisterForConnectionDispose(persistence);
  345. items[AuthPersistenceKey] = persistence;
  346. return persistence;
  347. }
  348. private IDictionary<object, object> GetConnectionItems()
  349. {
  350. return Context.Features.Get<IConnectionItemsFeature>()?.Items
  351. ?? throw new NotSupportedException($"Negotiate authentication requires a server that supports {nameof(IConnectionItemsFeature)} like Kestrel.");
  352. }
  353. private void RegisterForConnectionDispose(IDisposable authState)
  354. {
  355. var connectionCompleteFeature = Context.Features.Get<IConnectionCompleteFeature>()
  356. ??throw new NotSupportedException($"Negotiate authentication requires a server that supports {nameof(IConnectionCompleteFeature)} like Kestrel.");
  357. connectionCompleteFeature.OnCompleted(DisposeState, authState);
  358. }
  359. private static Task DisposeState(object state)
  360. {
  361. ((IDisposable)state).Dispose();
  362. return Task.CompletedTask;
  363. }
  364. // This allows us to have one disposal registration per connection and limits churn on the Items collection.
  365. private class AuthPersistence : IDisposable
  366. {
  367. internal INegotiateState State { get; set; }
  368. public void Dispose()
  369. {
  370. State?.Dispose();
  371. }
  372. }
  373. }
  374. }