From e15fe540a87ba52129164d6f2291acda97012974 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 24 Jun 2014 10:17:29 -0700 Subject: [PATCH] Enable custom auth challenges. Integrate IAuthenticationHandler. --- .../AuthenticationHandler.cs | 148 +++++++++++ .../FeatureContext.cs | 10 +- .../Microsoft.AspNet.Server.WebListener.kproj | 1 + .../AuthenticationManager.cs | 53 +++- .../AuthenticationTypes.cs | 4 +- .../RequestProcessing/RequestContext.cs | 11 +- .../RequestProcessing/Response.cs | 2 +- src/Microsoft.Net.Server/WebListener.cs | 3 +- .../AuthenticationTests.cs | 230 +++++++++++++++++- .../AuthenticationTests.cs | 37 ++- 10 files changed, 453 insertions(+), 46 deletions(-) create mode 100644 src/Microsoft.AspNet.Server.WebListener/AuthenticationHandler.cs diff --git a/src/Microsoft.AspNet.Server.WebListener/AuthenticationHandler.cs b/src/Microsoft.AspNet.Server.WebListener/AuthenticationHandler.cs new file mode 100644 index 0000000000..a6a8afe19f --- /dev/null +++ b/src/Microsoft.AspNet.Server.WebListener/AuthenticationHandler.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.Net.Server; + +namespace Microsoft.AspNet.Server.WebListener +{ + internal class AuthenticationHandler : IAuthenticationHandler + { + private RequestContext _requestContext; + private AuthenticationTypes _authTypes; + private AuthenticationTypes _customChallenges; + + internal AuthenticationHandler(RequestContext requestContext) + { + _requestContext = requestContext; + _authTypes = requestContext.AuthenticationChallenges; + _customChallenges = AuthenticationTypes.None; + } + + public void Authenticate(IAuthenticateContext context) + { + var user = _requestContext.User; + var identity = user == null ? null : (ClaimsIdentity)user.Identity; + + foreach (var authType in ListEnabledAuthTypes()) + { + string authString = authType.ToString(); + if (context.AuthenticationTypes.Contains(authString, StringComparer.Ordinal)) + { + if (identity != null && identity.IsAuthenticated + && string.Equals(authString, identity.AuthenticationType, StringComparison.Ordinal)) + { + context.Authenticated((ClaimsIdentity)user.Identity, properties: null, description: GetDescription(user.Identity.AuthenticationType)); + } + else + { + context.NotAuthenticated(authString, properties: null, description: GetDescription(user.Identity.AuthenticationType)); + } + } + } + } + + public Task AuthenticateAsync(IAuthenticateContext context) + { + Authenticate(context); + return Task.FromResult(0); + } + + public void Challenge(IChallengeContext context) + { + foreach (var authType in ListEnabledAuthTypes()) + { + var authString = authType.ToString(); + // Not including any auth types means it's a blanket challenge for any auth type. + if (context.AuthenticationTypes == null || context.AuthenticationTypes.Count == 0 + || context.AuthenticationTypes.Contains(authString, StringComparer.Ordinal)) + { + _customChallenges |= authType; + context.Accept(authString, GetDescription(authType.ToString())); + } + } + // A challenge was issued, it overrides any pre-set auth types. + _requestContext.AuthenticationChallenges = _customChallenges; + } + + public void GetDescriptions(IAuthTypeContext context) + { + // TODO: Caching, this data doesn't change per request. + foreach (var authType in ListEnabledAuthTypes()) + { + context.Accept(GetDescription(authType.ToString())); + } + } + + public void SignIn(ISignInContext context) + { + // Not supported + } + + public void SignOut(ISignOutContext context) + { + // Not supported + } + + private IDictionary GetDescription(string authenticationType) + { + return new Dictionary() + { + { "AuthenticationType", authenticationType }, + { "Caption", "Windows:" + authenticationType }, + }; + } + + private IEnumerable ListEnabledAuthTypes() + { + // Order by strength. + if ((_authTypes & AuthenticationTypes.Kerberos) == AuthenticationTypes.Kerberos) + { + yield return AuthenticationTypes.Kerberos; + } + if ((_authTypes & AuthenticationTypes.Negotiate) == AuthenticationTypes.Negotiate) + { + yield return AuthenticationTypes.Negotiate; + } + if ((_authTypes & AuthenticationTypes.NTLM) == AuthenticationTypes.NTLM) + { + yield return AuthenticationTypes.NTLM; + } + /*if ((_authTypes & AuthenticationTypes.Digest) == AuthenticationTypes.Digest) + { + // TODO: + throw new NotImplementedException("Digest challenge generation has not been implemented."); + yield return AuthenticationTypes.Digest; + }*/ + if ((_authTypes & AuthenticationTypes.Basic) == AuthenticationTypes.Basic) + { + yield return AuthenticationTypes.Basic; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs index f1876bcf89..f028582f03 100644 --- a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs +++ b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs @@ -70,6 +70,7 @@ namespace Microsoft.AspNet.Server.WebListener { _requestContext = requestContext; _features = new FeatureCollection(); + _authHandler = new AuthenticationHandler(requestContext); PopulateFeatures(); } @@ -109,14 +110,11 @@ namespace Microsoft.AspNet.Server.WebListener _features.Add(typeof(IHttpWebSocketFeature), this); } - // TODO: - // _environment.CallCancelled = _cts.Token; - // _environment.User = _request.User; - // Channel binding - + // TODO: /* - // Server + Server _environment.Listener = _server; + Channel binding _environment.ConnectionId = _request.ConnectionId; */ } diff --git a/src/Microsoft.AspNet.Server.WebListener/Microsoft.AspNet.Server.WebListener.kproj b/src/Microsoft.AspNet.Server.WebListener/Microsoft.AspNet.Server.WebListener.kproj index 1b81c12fac..e4001830ea 100644 --- a/src/Microsoft.AspNet.Server.WebListener/Microsoft.AspNet.Server.WebListener.kproj +++ b/src/Microsoft.AspNet.Server.WebListener/Microsoft.AspNet.Server.WebListener.kproj @@ -21,6 +21,7 @@ + diff --git a/src/Microsoft.Net.Server/AuthenticationManager.cs b/src/Microsoft.Net.Server/AuthenticationManager.cs index 64ee0b4d7d..e7b1ab6e71 100644 --- a/src/Microsoft.Net.Server/AuthenticationManager.cs +++ b/src/Microsoft.Net.Server/AuthenticationManager.cs @@ -65,6 +65,10 @@ namespace Microsoft.Net.Server } set { + if (_authTypes == AuthenticationTypes.None) + { + throw new ArgumentException("value", "'None' is not a valid authentication type. Use 'AllowAnonymous' instead."); + } _authTypes = value; SetServerSecurity(); } @@ -106,22 +110,25 @@ namespace Microsoft.Net.Server } } - // TODO: If we're not going to support Digest then this whole list can be pre-computed and cached. - // consider even pre-serialzing and caching the bytes for the !AllowAnonymous scenario. - internal IList GenerateChallenges() + internal static IList GenerateChallenges(AuthenticationTypes authTypes) { IList challenges = new List(); + if (authTypes == AuthenticationTypes.None) + { + return challenges; + } + // Order by strength. - if ((_authTypes & AuthenticationTypes.Kerberos) == AuthenticationTypes.Kerberos) + if ((authTypes & AuthenticationTypes.Kerberos) == AuthenticationTypes.Kerberos) { challenges.Add("Kerberos"); } - if ((_authTypes & AuthenticationTypes.Negotiate) == AuthenticationTypes.Negotiate) + if ((authTypes & AuthenticationTypes.Negotiate) == AuthenticationTypes.Negotiate) { challenges.Add("Negotiate"); } - if ((_authTypes & AuthenticationTypes.Ntlm) == AuthenticationTypes.Ntlm) + if ((authTypes & AuthenticationTypes.NTLM) == AuthenticationTypes.NTLM) { challenges.Add("NTLM"); } @@ -131,7 +138,7 @@ namespace Microsoft.Net.Server throw new NotImplementedException("Digest challenge generation has not been implemented."); // challenges.Add("Digest"); }*/ - if ((_authTypes & AuthenticationTypes.Basic) == AuthenticationTypes.Basic) + if ((authTypes & AuthenticationTypes.Basic) == AuthenticationTypes.Basic) { // TODO: Realm challenges.Add("Basic"); @@ -139,9 +146,9 @@ namespace Microsoft.Net.Server return challenges; } - internal void SetAuthenticationChallenge(Response response) + internal void SetAuthenticationChallenge(RequestContext context) { - IList challenges = GenerateChallenges(); + IList challenges = GenerateChallenges(context.AuthenticationChallenges); if (challenges.Count > 0) { @@ -149,7 +156,7 @@ namespace Microsoft.Net.Server // Append to the existing header, if any. Some clients (IE, Chrome) require each challenges to be sent on their own line/header. string[] oldValues; string[] newValues; - if (response.Headers.TryGetValue(HttpKnownHeaderNames.WWWAuthenticate, out oldValues)) + if (context.Response.Headers.TryGetValue(HttpKnownHeaderNames.WWWAuthenticate, out oldValues)) { newValues = new string[oldValues.Length + challenges.Count]; Array.Copy(oldValues, newValues, oldValues.Length); @@ -160,7 +167,7 @@ namespace Microsoft.Net.Server newValues = new string[challenges.Count]; challenges.CopyTo(newValues, 0); } - response.Headers[HttpKnownHeaderNames.WWWAuthenticate] = newValues; + context.Response.Headers[HttpKnownHeaderNames.WWWAuthenticate] = newValues; } } @@ -184,10 +191,30 @@ namespace Microsoft.Net.Server && requestInfo->pInfo->AuthStatus == UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) { #if NET45 - return new WindowsPrincipal(new WindowsIdentity(requestInfo->pInfo->AccessToken)); + return new WindowsPrincipal(new WindowsIdentity(requestInfo->pInfo->AccessToken, + GetAuthTypeFromRequest(requestInfo->pInfo->AuthType).ToString())); #endif } - return new ClaimsPrincipal(new ClaimsIdentity(string.Empty)); // Anonymous / !IsAuthenticated + return new ClaimsPrincipal(new ClaimsIdentity()); // Anonymous / !IsAuthenticated + } + + private static AuthenticationTypes GetAuthTypeFromRequest(UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE input) + { + switch (input) + { + case UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeBasic: + return AuthenticationTypes.Basic; + // case UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeDigest: + // return AuthenticationTypes.Digest; + case UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNTLM: + return AuthenticationTypes.NTLM; + case UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNegotiate: + return AuthenticationTypes.Negotiate; + case UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeKerberos: + return AuthenticationTypes.Kerberos; + default: + throw new NotImplementedException(input.ToString()); + } } } } diff --git a/src/Microsoft.Net.Server/AuthenticationTypes.cs b/src/Microsoft.Net.Server/AuthenticationTypes.cs index e524d6b69c..9906ba2053 100644 --- a/src/Microsoft.Net.Server/AuthenticationTypes.cs +++ b/src/Microsoft.Net.Server/AuthenticationTypes.cs @@ -22,10 +22,10 @@ namespace Microsoft.Net.Server [Flags] public enum AuthenticationTypes { - // None = 0x0, // None is invalid, use AllowAnonymous (which must have a non-zero value). + None = 0x0, Basic = 0x1, // Digest = 0x2, // TODO: Verify this is no longer supported by Http.Sys - Ntlm = 0x4, + NTLM = 0x4, Negotiate = 0x8, Kerberos = 0x10, AllowAnonymous = 0x1000 diff --git a/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs b/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs index e0cc636bd1..3864652cef 100644 --- a/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs +++ b/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs @@ -45,14 +45,15 @@ namespace Microsoft.Net.Server private CancellationTokenSource _requestAbortSource; private CancellationToken? _disconnectToken; - internal RequestContext(WebListener httpListener, NativeRequestContext memoryBlob) + internal RequestContext(WebListener server, NativeRequestContext memoryBlob) { // TODO: Verbose log - _server = httpListener; + _server = server; _memoryBlob = memoryBlob; _request = new Request(this, _memoryBlob); _response = new Response(this); _request.ReleasePins(); + AuthenticationChallenges = server.AuthenticationManager.AuthenticationTypes & ~AuthenticationTypes.AllowAnonymous; } public Request Request @@ -129,6 +130,12 @@ namespace Microsoft.Net.Server } } + /// + /// The authentication challengest that will be added to the response if the status code is 401. + /// This must be a subset of the AuthenticationTypes enabled on the server. + /// + public AuthenticationTypes AuthenticationChallenges { get; set; } + public bool IsUpgradableRequest { get { return _request.IsUpgradable; } diff --git a/src/Microsoft.Net.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Server/RequestProcessing/Response.cs index 0bca230c20..d803bb4a23 100644 --- a/src/Microsoft.Net.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Server/RequestProcessing/Response.cs @@ -471,7 +471,7 @@ namespace Microsoft.Net.Server // 401 if (StatusCode == (ushort)HttpStatusCode.Unauthorized) { - RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(this); + RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext); } UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE; diff --git a/src/Microsoft.Net.Server/WebListener.cs b/src/Microsoft.Net.Server/WebListener.cs index a66dd103f8..66a38efdfc 100644 --- a/src/Microsoft.Net.Server/WebListener.cs +++ b/src/Microsoft.Net.Server/WebListener.cs @@ -661,7 +661,8 @@ namespace Microsoft.Net.Server var requestV2 = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2*)requestMemory.RequestBlob; if (!AuthenticationManager.AllowAnonymous && !AuthenticationManager.CheckAuthenticated(requestV2->pRequestInfo)) { - SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.Unauthorized, AuthenticationManager.GenerateChallenges()); + SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.Unauthorized, + AuthenticationManager.GenerateChallenges(AuthenticationManager.AuthenticationTypes)); return false; } return true; diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs index eae1a1e89e..db7417482d 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs @@ -16,6 +16,7 @@ // permissions and limitations under the License. using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -31,12 +32,13 @@ namespace Microsoft.AspNet.Server.WebListener private const string Address = "http://localhost:8080/"; [Theory] + [InlineData(AuthenticationTypes.AllowAnonymous)] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] [InlineData(AuthenticationTypes.Basic)] - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_AllowAnonymous_NoChallenge(AuthenticationTypes authType) { using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => @@ -56,7 +58,7 @@ namespace Microsoft.AspNet.Server.WebListener [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented [InlineData(AuthenticationTypes.Basic)] public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationTypes authType) @@ -75,7 +77,7 @@ namespace Microsoft.AspNet.Server.WebListener [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented [InlineData(AuthenticationTypes.Basic)] public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationTypes authType) @@ -101,7 +103,7 @@ namespace Microsoft.AspNet.Server.WebListener using (Utilities.CreateAuthServer( AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate - | AuthenticationTypes.Ntlm + | AuthenticationTypes.NTLM /* | AuthenticationTypes.Digest TODO: Not implemented */ | AuthenticationTypes.Basic | AuthenticationTypes.AllowAnonymous, @@ -123,10 +125,10 @@ namespace Microsoft.AspNet.Server.WebListener [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented // [InlineData(AuthenticationTypes.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /* AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /* AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationTypes authType) { int requestId = 0; @@ -159,10 +161,10 @@ namespace Microsoft.AspNet.Server.WebListener [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented // [InlineData(AuthenticationTypes.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /* AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /* AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_RequireAuth_Success(AuthenticationTypes authType) { using (Utilities.CreateAuthServer(authType, env => @@ -178,6 +180,216 @@ namespace Microsoft.AspNet.Server.WebListener } } + [Theory] + [InlineData(AuthenticationTypes.AllowAnonymous)] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + // [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + public async Task AuthTypes_GetSingleDescriptions(AuthenticationTypes authType) + { + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + var resultList = context.GetAuthenticationTypes(); + if (authType == AuthenticationTypes.AllowAnonymous) + { + Assert.Equal(0, resultList.Count()); + } + else + { + Assert.Equal(1, resultList.Count()); + var result = resultList.First(); + Assert.Equal(authType.ToString(), result.AuthenticationType); + Assert.Equal("Windows:" + authType.ToString(), result.Caption); + } + + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); + } + } + + [Fact] + public async Task AuthTypes_GetMultipleDescriptions() + { + AuthenticationTypes authType = + AuthenticationTypes.Kerberos + | AuthenticationTypes.Negotiate + | AuthenticationTypes.NTLM + | /*AuthenticationTypes.Digest + |*/ AuthenticationTypes.Basic; + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + var resultList = context.GetAuthenticationTypes(); + Assert.Equal(4, resultList.Count()); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + public async Task AuthTypes_AuthenticateWithNoUser_NoResults(AuthenticationTypes authType) + { + var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + var authResults = context.Authenticate(authTypeList); + Assert.False(authResults.Any()); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + // [InlineData(AuthenticationTypes.Basic)] // Doesn't work with default creds + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + public async Task AuthTypes_AuthenticateWithUser_OneResult(AuthenticationTypes authType) + { + var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + using (Utilities.CreateAuthServer(authType, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + var authResults = context.Authenticate(authTypeList); + Assert.Equal(1, authResults.Count()); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address, useDefaultCredentials: true); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + public async Task AuthTypes_ChallengeWithoutAuthTypes_AllChallengesSent(AuthenticationTypes authType) + { + var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.Challenge(); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authTypeList.Count(), response.Headers.WwwAuthenticate.Count); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + public async Task AuthTypes_ChallengeWithAllAuthTypes_AllChallengesSent(AuthenticationTypes authType) + { + var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.Challenge(authTypeList); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authTypeList.Count(), response.Headers.WwwAuthenticate.Count); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthTypes_ChallengeOneAuthType_OneChallengeSent(AuthenticationTypes authType) + { + var authTypes = AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic; + using (Utilities.CreateAuthServer(authTypes | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.Challenge(authType.ToString()); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(1, response.Headers.WwwAuthenticate.Count); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.First().Scheme); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.NTLM)] + // [InlineData(AuthenticationTypes.Digest)] + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthTypes_ChallengeDisabledAuthType_Error(AuthenticationTypes authType) + { + var authTypes = AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic; + authTypes = authTypes & ~authType; + var authTypeList = authType.ToString().Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + using (Utilities.CreateAuthServer(authTypes | AuthenticationTypes.AllowAnonymous, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.Throws(() => context.Response.Challenge(authType.ToString())); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); + } + } + private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) { HttpClientHandler handler = new HttpClientHandler(); diff --git a/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs index ce57a86d35..503b515b7e 100644 --- a/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs +++ b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs @@ -16,10 +16,10 @@ namespace Microsoft.Net.Server [InlineData(AuthenticationTypes.AllowAnonymous)] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] [InlineData(AuthenticationTypes.Basic)] - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationTypes.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_AllowAnonymous_NoChallenge(AuthenticationTypes authType) { using (var server = Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous)) @@ -29,6 +29,14 @@ namespace Microsoft.Net.Server var context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.False(context.User.Identity.IsAuthenticated); + if (authType == AuthenticationTypes.AllowAnonymous) + { + Assert.Equal(AuthenticationTypes.None, context.AuthenticationChallenges); + } + else + { + Assert.Equal(authType, context.AuthenticationChallenges); + } context.Dispose(); var response = await responseTask; @@ -40,7 +48,7 @@ namespace Microsoft.Net.Server [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented [InlineData(AuthenticationTypes.Basic)] public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationTypes authType) @@ -60,7 +68,7 @@ namespace Microsoft.Net.Server [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented [InlineData(AuthenticationTypes.Basic)] public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationTypes authType) @@ -72,6 +80,7 @@ namespace Microsoft.Net.Server var context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.AuthenticationChallenges); context.Response.StatusCode = 401; context.Dispose(); @@ -84,19 +93,20 @@ namespace Microsoft.Net.Server [Fact] public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded() { - using (var server = Utilities.CreateAuthServer( + AuthenticationTypes authType = AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate - | AuthenticationTypes.Ntlm + | AuthenticationTypes.NTLM /* | AuthenticationTypes.Digest TODO: Not implemented */ - | AuthenticationTypes.Basic - | AuthenticationTypes.AllowAnonymous)) + | AuthenticationTypes.Basic; + using (var server = Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous)) { Task responseTask = SendRequestAsync(Address); var context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.AuthenticationChallenges); context.Response.StatusCode = 401; context.Dispose(); @@ -109,10 +119,10 @@ namespace Microsoft.Net.Server [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented // [InlineData(AuthenticationTypes.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /*AuthenticationType.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationTypes authType) { using (var server = Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous)) @@ -122,12 +132,14 @@ namespace Microsoft.Net.Server var context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.False(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.AuthenticationChallenges); context.Response.StatusCode = 401; context.Dispose(); context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.True(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.AuthenticationChallenges); context.Dispose(); var response = await responseTask; @@ -138,10 +150,10 @@ namespace Microsoft.Net.Server [Theory] [InlineData(AuthenticationTypes.Kerberos)] [InlineData(AuthenticationTypes.Negotiate)] - [InlineData(AuthenticationTypes.Ntlm)] + [InlineData(AuthenticationTypes.NTLM)] // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented // [InlineData(AuthenticationTypes.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.Ntlm | /*AuthenticationType.Digest |*/ AuthenticationTypes.Basic)] + [InlineData(AuthenticationTypes.Kerberos | AuthenticationTypes.Negotiate | AuthenticationTypes.NTLM | /*AuthenticationType.Digest |*/ AuthenticationTypes.Basic)] public async Task AuthTypes_RequireAuth_Success(AuthenticationTypes authType) { using (var server = Utilities.CreateAuthServer(authType)) @@ -151,6 +163,7 @@ namespace Microsoft.Net.Server var context = await server.GetContextAsync(); Assert.NotNull(context.User); Assert.True(context.User.Identity.IsAuthenticated); + Assert.Equal(authType, context.AuthenticationChallenges); context.Dispose(); var response = await responseTask;