#1 Implement a full authentication handler.

This commit is contained in:
Chris R 2015-10-13 13:47:45 -07:00
parent 348ab7c943
commit 2fe2e0d841
9 changed files with 349 additions and 45 deletions

View File

@ -0,0 +1,138 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features.Authentication;
namespace Microsoft.AspNet.IISPlatformHandler
{
internal class AuthenticationHandler : IAuthenticationHandler
{
internal AuthenticationHandler(HttpContext httpContext, IISPlatformHandlerOptions options, ClaimsPrincipal user)
{
HttpContext = httpContext;
User = user;
Options = options;
}
internal HttpContext HttpContext { get; }
internal IISPlatformHandlerOptions Options { get; }
internal ClaimsPrincipal User { get; }
internal IAuthenticationHandler PriorHandler { get; set; }
public Task AuthenticateAsync(AuthenticateContext context)
{
if (ShouldHandleScheme(context.AuthenticationScheme))
{
if (User != null)
{
context.Authenticated(User, properties: null,
description: Options.AuthenticationDescriptions.Where(descrip =>
string.Equals(User.Identity.AuthenticationType, descrip.AuthenticationScheme, StringComparison.Ordinal)).FirstOrDefault()?.Items);
}
else
{
context.NotAuthenticated();
}
}
if (PriorHandler != null)
{
return PriorHandler.AuthenticateAsync(context);
}
return Task.FromResult(0);
}
public Task ChallengeAsync(ChallengeContext context)
{
bool handled = false;
if (ShouldHandleScheme(context.AuthenticationScheme))
{
switch (context.Behavior)
{
case ChallengeBehavior.Automatic:
// If there is a principal already, invoke the forbidden code path
if (User == null)
{
goto case ChallengeBehavior.Unauthorized;
}
else
{
goto case ChallengeBehavior.Forbidden;
}
case ChallengeBehavior.Unauthorized:
HttpContext.Response.StatusCode = 401;
// We would normally set the www-authenticate header here, but IIS does that for us.
break;
case ChallengeBehavior.Forbidden:
HttpContext.Response.StatusCode = 403;
handled = true; // No other handlers need to consider this challenge.
break;
}
context.Accept();
}
if (!handled && PriorHandler != null)
{
return PriorHandler.ChallengeAsync(context);
}
return Task.FromResult(0);
}
public void GetDescriptions(DescribeSchemesContext context)
{
foreach (var description in Options.AuthenticationDescriptions)
{
context.Accept(description.Items);
}
if (PriorHandler != null)
{
PriorHandler.GetDescriptions(context);
}
}
public Task SignInAsync(SignInContext context)
{
// Not supported, fall through
if (PriorHandler != null)
{
return PriorHandler.SignInAsync(context);
}
return Task.FromResult(0);
}
public Task SignOutAsync(SignOutContext context)
{
// Not supported, fall through
if (PriorHandler != null)
{
return PriorHandler.SignOutAsync(context);
}
return Task.FromResult(0);
}
private bool ShouldHandleScheme(string authenticationScheme)
{
if (Options.AutomaticAuthentication && string.IsNullOrEmpty(authenticationScheme))
{
return true;
}
foreach (var description in Options.AuthenticationDescriptions)
{
if (string.Equals(description.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.IISPlatformHandler
{
public class IISPlatformHandlerDefaults
{
public const string Negotiate = "Negotiate";
public const string Ntlm = "NTLM";
}
}

View File

@ -3,13 +3,18 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.AspNet.Http.Features.Authentication.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.IISPlatformHandler
{
@ -22,13 +27,44 @@ namespace Microsoft.AspNet.IISPlatformHandler
private const string XOriginalIPName = "X-Original-IP";
private readonly RequestDelegate _next;
private readonly IISPlatformHandlerOptions _options;
public IISPlatformHandlerMiddleware(RequestDelegate next)
public IISPlatformHandlerMiddleware(RequestDelegate next, IISPlatformHandlerOptions options)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_next = next;
_options = options;
}
public Task Invoke(HttpContext httpContext)
public async Task Invoke(HttpContext httpContext)
{
UpdateScheme(httpContext);
UpdateRemoteIp(httpContext);
var winPrincipal = UpdateUser(httpContext);
var handler = new AuthenticationHandler(httpContext, _options, winPrincipal);
AttachAuthenticationHandler(handler);
try
{
await _next(httpContext);
}
finally
{
DetachAuthenticationhandler(handler);
}
}
private static void UpdateScheme(HttpContext httpContext)
{
var xForwardProtoHeaderValue = httpContext.Request.Headers[XForwardedProtoHeaderName];
if (!string.IsNullOrEmpty(xForwardProtoHeaderValue))
@ -39,7 +75,10 @@ namespace Microsoft.AspNet.IISPlatformHandler
}
httpContext.Request.Scheme = xForwardProtoHeaderValue;
}
}
private static void UpdateRemoteIp(HttpContext httpContext)
{
var xForwardedForHeaderValue = httpContext.Request.Headers.GetCommaSeparatedValues(XForwardedForHeaderName);
if (xForwardedForHeaderValue != null && xForwardedForHeaderValue.Length > 0)
{
@ -54,32 +93,62 @@ namespace Microsoft.AspNet.IISPlatformHandler
httpContext.Connection.RemoteIpAddress = ipFromHeader;
}
}
}
private WindowsPrincipal UpdateUser(HttpContext httpContext)
{
var xIISWindowsAuthToken = httpContext.Request.Headers[XIISWindowsAuthToken];
int hexHandle;
WindowsPrincipal winPrincipal = null;
if (!StringValues.IsNullOrEmpty(xIISWindowsAuthToken)
&& int.TryParse(xIISWindowsAuthToken, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out hexHandle))
{
// Always create the identity if the handle exists, we need to dispose it so it does not leak.
var handle = new IntPtr(hexHandle);
var winIdentity = new WindowsIdentity(handle);
// WindowsIdentity just duplicated the handle so we need to close the original.
NativeMethods.CloseHandle(handle);
httpContext.Response.RegisterForDispose(winIdentity);
var winPrincipal = new WindowsPrincipal(winIdentity);
winPrincipal = new WindowsPrincipal(winIdentity);
var existingPrincipal = httpContext.User;
if (existingPrincipal != null)
if (_options.AutomaticAuthentication)
{
httpContext.User = SecurityHelper.MergeUserPrincipal(existingPrincipal, winPrincipal);
}
else
{
httpContext.User = winPrincipal;
var existingPrincipal = httpContext.User;
if (existingPrincipal != null)
{
httpContext.User = SecurityHelper.MergeUserPrincipal(existingPrincipal, winPrincipal);
}
else
{
httpContext.User = winPrincipal;
}
}
}
return _next(httpContext);
return winPrincipal;
}
private void AttachAuthenticationHandler(AuthenticationHandler handler)
{
var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
if (auth == null)
{
auth = new HttpAuthenticationFeature();
handler.HttpContext.Features.Set(auth);
}
handler.PriorHandler = auth.Handler;
auth.Handler = handler;
}
private void DetachAuthenticationhandler(AuthenticationHandler handler)
{
var auth = handler.HttpContext.Features.Get<IHttpAuthenticationFeature>();
if (auth != null)
{
auth.Handler = handler.PriorHandler;
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.IISPlatformHandler;
namespace Microsoft.AspNet.Builder
@ -13,9 +14,56 @@ namespace Microsoft.AspNet.Builder
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseIISPlatformHandler(this IApplicationBuilder builder)
public static IApplicationBuilder UseIISPlatformHandler(this IApplicationBuilder app)
{
return builder.UseMiddleware<IISPlatformHandlerMiddleware>();
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<IISPlatformHandlerMiddleware>(new IISPlatformHandlerOptions());
}
/// <summary>
/// Adds middleware for interacting with the IIS HttpPlatformHandler reverse proxy module.
/// This will handle forwarded Windows Authentication, request scheme, remote IPs, etc..
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseIISPlatformHandler(this IApplicationBuilder app, IISPlatformHandlerOptions options)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
return app.UseMiddleware<IISPlatformHandlerMiddleware>(options);
}
/// <summary>
/// Adds middleware for interacting with the IIS HttpPlatformHandler reverse proxy module.
/// This will handle forwarded Windows Authentication, request scheme, remote IPs, etc..
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseIISPlatformHandler(this IApplicationBuilder app, Action<IISPlatformHandlerOptions> configureOptions)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var options = new IISPlatformHandlerOptions();
if (configureOptions != null)
{
configureOptions(options);
}
return app.UseIISPlatformHandler(options);
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Http.Authentication;
namespace Microsoft.AspNet.IISPlatformHandler
{
public class IISPlatformHandlerOptions
{
/// <summary>
/// If true the authentication middleware alter the request user coming in and respond to generic challenges.
/// If false the authentication middleware will only provide identity and respond to challenges when explicitly indicated
/// by the AuthenticationScheme.
/// </summary>
public bool AutomaticAuthentication { get; set; } = true;
/// <summary>
/// Additional information about the authentication type which is made available to the application.
/// </summary>
public IList<AuthenticationDescription> AuthenticationDescriptions { get; } = new List<AuthenticationDescription>()
{
new AuthenticationDescription()
{
AuthenticationScheme = IISPlatformHandlerDefaults.Negotiate,
DisplayName = IISPlatformHandlerDefaults.Negotiate
},
new AuthenticationDescription()
{
AuthenticationScheme = IISPlatformHandlerDefaults.Ntlm,
DisplayName = IISPlatformHandlerDefaults.Ntlm
}
};
}
}

View File

@ -4,17 +4,15 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>abe53415-83ce-4af0-af67-e52160c7862b</ProjectGuid>
<RootNamespace>Microsoft.AspNet.PlatformHandler</RootNamespace>
<RootNamespace>Microsoft.AspNet.IISPlatformHandler</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
</Project>

View File

@ -6,6 +6,7 @@
"url": "git://github.com/aspnet/IISIntegration"
},
"dependencies": {
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.Extensions.SecurityHelper.Sources": {
"type": "build",

View File

@ -1027,12 +1027,11 @@
</system.webServer>
</location>
<!-- Prabht -->
<location path="NtlmAuthenticationTestSite">
<system.webServer>
<security>
<authentication>
<anonymousAuthentication enabled="false" />
<anonymousAuthentication enabled="true" />
<windowsAuthentication enabled="true" />
</authentication>
</security>

View File

@ -17,8 +17,7 @@ namespace Microsoft.AspNet.IISPlatformHandler.FunctionalTests
// Uses ports ranging 5050 - 5060.
public class NtlmAuthenticationTests
{
// TODO: The middleware needs to implement auth handlers.
[ConditionalTheory, Trait("ServerComparison.FunctionalTests", "ServerComparison.FunctionalTests")]
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x86, "http://localhost:5050/")]
@ -42,7 +41,7 @@ namespace Microsoft.AspNet.IISPlatformHandler.FunctionalTests
using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, logger))
{
var deploymentResult = deployer.Deploy();
var httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true };
var httpClientHandler = new HttpClientHandler();
var httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(deploymentResult.ApplicationBaseUri) };
// Request to base address and check if various parts of the body are rendered & measure the cold startup time.
@ -54,54 +53,60 @@ namespace Microsoft.AspNet.IISPlatformHandler.FunctionalTests
var responseText = await response.Content.ReadAsStringAsync();
try
{
// TODO: Currently we do not implement mixed auth.
// https://github.com/aspnet/IISIntegration/issues/1
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString());
// Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString());
/*
Assert.Equal("Hello World", responseText);
responseText = await httpClient.GetStringAsync("/Anonymous");
response = await httpClient.GetAsync("/Anonymous");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Anonymous?True", responseText);
response = await httpClient.GetAsync("/Restricted");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString());
Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString());
response = await httpClient.GetAsync("/RestrictedNTLM");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Contains("NTLM", response.Headers.WwwAuthenticate.ToString());
// Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves.
Assert.Contains("Negotiate", response.Headers.WwwAuthenticate.ToString());
response = await httpClient.GetAsync("/Forbidden");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
*/
// httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true };
// httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(deploymentResult.ApplicationBaseUri) };
responseText = await httpClient.GetStringAsync("/Anonymous");
Assert.Equal("Anonymous?False", responseText);
httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true };
httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(deploymentResult.ApplicationBaseUri) };
response = await httpClient.GetAsync("/Anonymous");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Anonymous?True", responseText);
/*
response = await httpClient.GetAsync("/AutoForbid");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
responseText = await httpClient.GetStringAsync("/Restricted");
Assert.Equal("Negotiate", responseText);
response = await httpClient.GetAsync("/Restricted");
responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("Kerberos", responseText);
responseText = await httpClient.GetStringAsync("/RestrictedNegotiate");
Assert.Equal("Negotiate", responseText);
response = await httpClient.GetAsync("/RestrictedNTLM");
// This isn't a Forbidden because we authenticate with Negotiate and challenge for NTLM.
response = await httpClient.GetAsync("/RestrictedNegotiate");
responseText = await response.Content.ReadAsStringAsync();
// This is Forbidden because we authenticate with Kerberos and challenge for Negotiate.
// Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves,
// so both Negotiate and NTLM get sent again.
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
*/
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
response = await httpClient.GetAsync("/RestrictedNTLM");
responseText = await response.Content.ReadAsStringAsync();
// This is Forbidden because we authenticate with Kerberos and challenge for NTLM.
// Note we can't restrict a challenge to a specific auth type, the native auth modules always add themselves,
// so both Negotiate and NTLM get sent again.
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
catch (XunitException)
{