diff --git a/samples/SelfHostServer/Startup.cs b/samples/SelfHostServer/Startup.cs index 3a6c87a4ab..917c06a5b7 100644 --- a/samples/SelfHostServer/Startup.cs +++ b/samples/SelfHostServer/Startup.cs @@ -32,7 +32,7 @@ namespace SelfHostServer public void Configure(IBuilder app) { var info = (ServerInformation)app.Server; - info.Listener.AuthenticationManager.AuthenticationTypes = AuthenticationType.None; + info.Listener.AuthenticationManager.AuthenticationTypes = AuthenticationTypes.AllowAnonymous; app.Run(async context => { diff --git a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs index 136d3ab552..f1876bcf89 100644 --- a/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs +++ b/src/Microsoft.AspNet.Server.WebListener/FeatureContext.cs @@ -20,11 +20,14 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.WebSockets; +using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.HttpFeature.Security; using Microsoft.Net.Server; using Microsoft.Net.WebSockets; @@ -38,6 +41,7 @@ namespace Microsoft.AspNet.Server.WebListener IHttpClientCertificateFeature, IHttpRequestLifetimeFeature, IHttpWebSocketFeature, + IHttpAuthenticationFeature, IHttpOpaqueUpgradeFeature { private RequestContext _requestContext; @@ -57,6 +61,8 @@ namespace Microsoft.AspNet.Server.WebListener private int? _localPort; private bool? _isLocal; private X509Certificate _clientCert; + private ClaimsPrincipal _user; + private IAuthenticationHandler _authHandler; private Stream _responseStream; private IDictionary _responseHeaders; @@ -94,6 +100,7 @@ namespace Microsoft.AspNet.Server.WebListener _features.Add(typeof(IHttpResponseFeature), this); _features.Add(typeof(IHttpSendFileFeature), this); _features.Add(typeof(IHttpRequestLifetimeFeature), this); + _features.Add(typeof(IHttpAuthenticationFeature), this); // Win8+ if (WebSocketHelpers.AreWebSocketsSupported) @@ -403,5 +410,25 @@ namespace Microsoft.AspNet.Server.WebListener } return _requestContext.AcceptWebSocketAsync(subProtocol); } + + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get + { + if (_user == null) + { + _user = _requestContext.User; + } + return _user; + } + set { _user = value; } + } + + // TODO: Hook this server up as the default handler, have it issue challenges for configured auth types by name. + IAuthenticationHandler IHttpAuthenticationFeature.Handler + { + get { return _authHandler; } + set { _authHandler = value; } + } } } diff --git a/src/Microsoft.AspNet.Server.WebListener/project.json b/src/Microsoft.AspNet.Server.WebListener/project.json index d7d1049954..f28e0e19e3 100644 --- a/src/Microsoft.AspNet.Server.WebListener/project.json +++ b/src/Microsoft.AspNet.Server.WebListener/project.json @@ -33,6 +33,7 @@ "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.Handles": "4.0.0.0", "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Claims": "0.1-alpha-*", "System.Security.Cryptography.X509Certificates": "4.0.0.0", "System.Security.Principal": "4.0.0.0", "System.Text.Encoding": "4.0.20.0", diff --git a/src/Microsoft.Net.Server/AsyncAcceptContext.cs b/src/Microsoft.Net.Server/AsyncAcceptContext.cs index 71cc7952e0..f1df0bbbad 100644 --- a/src/Microsoft.Net.Server/AsyncAcceptContext.cs +++ b/src/Microsoft.Net.Server/AsyncAcceptContext.cs @@ -91,7 +91,7 @@ namespace Microsoft.Net.Server bool stoleBlob = false; try { - if (server.ValidateRequest(asyncResult._nativeRequestContext)) + if (server.ValidateRequest(asyncResult._nativeRequestContext) && server.ValidateAuth(asyncResult._nativeRequestContext)) { stoleBlob = true; RequestContext requestContext = new RequestContext(server, asyncResult._nativeRequestContext); diff --git a/src/Microsoft.Net.Server/AuthenticationManager.cs b/src/Microsoft.Net.Server/AuthenticationManager.cs index ec3452357e..64ee0b4d7d 100644 --- a/src/Microsoft.Net.Server/AuthenticationManager.cs +++ b/src/Microsoft.Net.Server/AuthenticationManager.cs @@ -25,6 +25,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.Claims; +using System.Security.Principal; namespace Microsoft.Net.Server { @@ -45,17 +47,17 @@ namespace Microsoft.Net.Server #endif private WebListener _server; - AuthenticationType _authTypes; + private AuthenticationTypes _authTypes; internal AuthenticationManager(WebListener listener) { _server = listener; - _authTypes = AuthenticationType.None; + _authTypes = AuthenticationTypes.AllowAnonymous; } #region Properties - public AuthenticationType AuthenticationTypes + public AuthenticationTypes AuthenticationTypes { get { @@ -68,6 +70,14 @@ namespace Microsoft.Net.Server } } + internal bool AllowAnonymous + { + get + { + return ((_authTypes & AuthenticationTypes.AllowAnonymous) == AuthenticationTypes.AllowAnonymous); + } + } + #endregion Properties private unsafe void SetServerSecurity() @@ -76,71 +86,108 @@ namespace Microsoft.Net.Server new UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_AUTHENTICATION_INFO(); authInfo.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_PROPERTY_FLAG_PRESENT; - authInfo.AuthSchemes = (UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_TYPES)_authTypes; + var authTypes = (UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_TYPES)(_authTypes & ~AuthenticationTypes.AllowAnonymous); + if (authTypes != UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_TYPES.NONE) + { + authInfo.AuthSchemes = authTypes; - // TODO: - // NTLM auth sharing (on by default?) DisableNTLMCredentialCaching - // Kerberos auth sharing (off by default?) HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING - // Mutual Auth - ReceiveMutualAuth - // Digest domain and realm - HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS - // Basic realm - HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS + // TODO: + // NTLM auth sharing (on by default?) DisableNTLMCredentialCaching + // Kerberos auth sharing (off by default?) HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING + // Mutual Auth - ReceiveMutualAuth + // Digest domain and realm - HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS + // Basic realm - HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS - IntPtr infoptr = new IntPtr(&authInfo); + IntPtr infoptr = new IntPtr(&authInfo); - _server.SetUrlGroupProperty( - UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty, - infoptr, (uint)AuthInfoSize); + _server.SetUrlGroupProperty( + UnsafeNclNativeMethods.HttpApi.HTTP_SERVER_PROPERTY.HttpServerAuthenticationProperty, + infoptr, (uint)AuthInfoSize); + } } - internal void SetAuthenticationChallenge(Response response) + // 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() { - if (_authTypes == AuthenticationType.None) - { - return; - } - IList challenges = new List(); // Order by strength. - if ((_authTypes & AuthenticationType.Kerberos) == AuthenticationType.Kerberos) + if ((_authTypes & AuthenticationTypes.Kerberos) == AuthenticationTypes.Kerberos) { challenges.Add("Kerberos"); } - if ((_authTypes & AuthenticationType.Negotiate) == AuthenticationType.Negotiate) + if ((_authTypes & AuthenticationTypes.Negotiate) == AuthenticationTypes.Negotiate) { challenges.Add("Negotiate"); } - if ((_authTypes & AuthenticationType.Ntlm) == AuthenticationType.Ntlm) + if ((_authTypes & AuthenticationTypes.Ntlm) == AuthenticationTypes.Ntlm) { challenges.Add("NTLM"); } - if ((_authTypes & AuthenticationType.Digest) == AuthenticationType.Digest) + /*if ((_authTypes & AuthenticationTypes.Digest) == AuthenticationTypes.Digest) { // TODO: throw new NotImplementedException("Digest challenge generation has not been implemented."); // challenges.Add("Digest"); - } - if ((_authTypes & AuthenticationType.Basic) == AuthenticationType.Basic) + }*/ + if ((_authTypes & AuthenticationTypes.Basic) == AuthenticationTypes.Basic) { // TODO: Realm challenges.Add("Basic"); } + return challenges; + } - // 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)) + internal void SetAuthenticationChallenge(Response response) + { + IList challenges = GenerateChallenges(); + + if (challenges.Count > 0) { - newValues = new string[oldValues.Length + challenges.Count]; - Array.Copy(oldValues, newValues, oldValues.Length); - challenges.CopyTo(newValues, oldValues.Length); + // TODO: We need a better header API that just lets us append values. + // 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)) + { + newValues = new string[oldValues.Length + challenges.Count]; + Array.Copy(oldValues, newValues, oldValues.Length); + challenges.CopyTo(newValues, oldValues.Length); + } + else + { + newValues = new string[challenges.Count]; + challenges.CopyTo(newValues, 0); + } + response.Headers[HttpKnownHeaderNames.WWWAuthenticate] = newValues; } - else + } + + internal static unsafe bool CheckAuthenticated(UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO* requestInfo) + { + if (requestInfo != null + && requestInfo->InfoType == UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth + && requestInfo->pInfo->AuthStatus == UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) { - newValues = new string[challenges.Count]; - challenges.CopyTo(newValues, 0); +#if NET45 + return true; +#endif } - response.Headers[HttpKnownHeaderNames.WWWAuthenticate] = newValues; + return false; + } + + internal static unsafe ClaimsPrincipal GetUser(UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO* requestInfo) + { + if (requestInfo != null + && requestInfo->InfoType == UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth + && requestInfo->pInfo->AuthStatus == UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) + { +#if NET45 + return new WindowsPrincipal(new WindowsIdentity(requestInfo->pInfo->AccessToken)); +#endif + } + return new ClaimsPrincipal(new ClaimsIdentity(string.Empty)); // Anonymous / !IsAuthenticated } } } diff --git a/src/Microsoft.Net.Server/AuthenticationTypes.cs b/src/Microsoft.Net.Server/AuthenticationTypes.cs index 73e07811bd..e524d6b69c 100644 --- a/src/Microsoft.Net.Server/AuthenticationTypes.cs +++ b/src/Microsoft.Net.Server/AuthenticationTypes.cs @@ -16,21 +16,18 @@ // permissions and limitations under the License. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.Net.Server { [Flags] - public enum AuthenticationType + public enum AuthenticationTypes { - None = 0x0, + // None = 0x0, // None is invalid, use AllowAnonymous (which must have a non-zero value). Basic = 0x1, - Digest = 0x2, + // Digest = 0x2, // TODO: Verify this is no longer supported by Http.Sys Ntlm = 0x4, Negotiate = 0x8, Kerberos = 0x10, + AllowAnonymous = 0x1000 } } diff --git a/src/Microsoft.Net.Server/RequestProcessing/Request.cs b/src/Microsoft.Net.Server/RequestProcessing/Request.cs index ba535acea1..513f5a4f39 100644 --- a/src/Microsoft.Net.Server/RequestProcessing/Request.cs +++ b/src/Microsoft.Net.Server/RequestProcessing/Request.cs @@ -27,9 +27,11 @@ using System.Globalization; using System.IO; using System.Net; using System.Runtime.InteropServices; +using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +#if NET45 using System.Security.Principal; -using System.Threading; +#endif using System.Threading.Tasks; namespace Microsoft.Net.Server @@ -66,7 +68,7 @@ namespace Microsoft.Net.Server private SocketAddress _localEndPoint; private SocketAddress _remoteEndPoint; - private IPrincipal _user; + private ClaimsPrincipal _user; private bool _isDisposed = false; @@ -140,7 +142,7 @@ namespace Microsoft.Net.Server _headers = new RequestHeaders(_nativeRequestContext); UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2* requestV2 = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2*)memoryBlob.RequestBlob; - _user = GetUser(requestV2->pRequestInfo); + _user = AuthenticationManager.GetUser(requestV2->pRequestInfo); // TODO: Verbose log parameters @@ -411,31 +413,11 @@ namespace Microsoft.Net.Server } } - internal IPrincipal User + internal ClaimsPrincipal User { get { return _user; } } - private unsafe IPrincipal GetUser(UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO* requestInfo) - { - if (requestInfo == null - || requestInfo->InfoType != UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth) - { - return null; - } - - if (requestInfo->pInfo->AuthStatus != UnsafeNclNativeMethods.HttpApi.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) - { - return null; - } - -#if NET45 - return new WindowsPrincipal(new WindowsIdentity(requestInfo->pInfo->AccessToken)); -#else - return null; -#endif - } - internal UnsafeNclNativeMethods.HttpApi.HTTP_VERB GetKnownMethod() { return UnsafeNclNativeMethods.HttpApi.GetKnownVerb(RequestBuffer, OriginalBlobAddress); diff --git a/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs b/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs index 07f0df4fab..e0cc636bd1 100644 --- a/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs +++ b/src/Microsoft.Net.Server/RequestProcessing/RequestContext.cs @@ -22,13 +22,12 @@ //------------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.WebSockets; using System.Runtime.InteropServices; -using System.Security.Principal; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Framework.Logging; @@ -72,7 +71,7 @@ namespace Microsoft.Net.Server } } - public IPrincipal User + public ClaimsPrincipal User { get { return _request.User; } } diff --git a/src/Microsoft.Net.Server/RequestProcessing/Response.cs b/src/Microsoft.Net.Server/RequestProcessing/Response.cs index 22d3d19f71..0bca230c20 100644 --- a/src/Microsoft.Net.Server/RequestProcessing/Response.cs +++ b/src/Microsoft.Net.Server/RequestProcessing/Response.cs @@ -708,7 +708,12 @@ namespace Microsoft.Net.Server } knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders; - knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = (uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); + knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = +#if NET45 + (uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); +#else + (uint)Marshal.SizeOf(); +#endif UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS(); diff --git a/src/Microsoft.Net.Server/WebListener.cs b/src/Microsoft.Net.Server/WebListener.cs index ba54332fde..a66dd103f8 100644 --- a/src/Microsoft.Net.Server/WebListener.cs +++ b/src/Microsoft.Net.Server/WebListener.cs @@ -78,6 +78,7 @@ namespace Microsoft.Net.Server private SafeHandle _requestQueueHandle; private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. + private bool _ignoreWriteExceptions; private HttpServerSessionHandle _serverSessionHandle; private ulong _urlGroupId; @@ -649,7 +650,18 @@ namespace Microsoft.Net.Server // Block potential DOS attacks if (requestMemory.RequestBlob->Headers.UnknownHeaderCount > UnknownHeaderLimit) { - SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.BadRequest); + SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.BadRequest, authChallenges: null); + return false; + } + return true; + } + + internal unsafe bool ValidateAuth(NativeRequestContext requestMemory) + { + var requestV2 = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2*)requestMemory.RequestBlob; + if (!AuthenticationManager.AllowAnonymous && !AuthenticationManager.CheckAuthenticated(requestV2->pRequestInfo)) + { + SendError(requestMemory.RequestBlob->RequestId, HttpStatusCode.Unauthorized, AuthenticationManager.GenerateChallenges()); return false; } return true; @@ -781,47 +793,116 @@ namespace Microsoft.Net.Server return cts.Token; } - private unsafe void SendError(ulong requestId, HttpStatusCode httpStatusCode) + private unsafe void SendError(ulong requestId, HttpStatusCode httpStatusCode, IList authChallenges) { UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 httpResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2(); httpResponse.Response_V1.Version = new UnsafeNclNativeMethods.HttpApi.HTTP_VERSION(); httpResponse.Response_V1.Version.MajorVersion = (ushort)1; httpResponse.Response_V1.Version.MinorVersion = (ushort)1; - httpResponse.Response_V1.StatusCode = (ushort)httpStatusCode; - string statusDescription = HttpReasonPhrase.Get(httpStatusCode); - uint dataWritten = 0; - uint statusCode; - byte[] byteReason = HeaderEncoding.GetBytes(statusDescription); - fixed (byte* pReason = byteReason) + + List pinnedHeaders = null; + GCHandle gcHandle; + try { - httpResponse.Response_V1.pReason = (sbyte*)pReason; - httpResponse.Response_V1.ReasonLength = (ushort)byteReason.Length; - - byte[] byteContentLength = new byte[] { (byte)'0' }; - fixed (byte* pContentLength = byteContentLength) + // Copied from the multi-value headers section of SerializeHeaders + if (authChallenges != null && authChallenges.Count > 0) { - (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = (sbyte*)pContentLength; - (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = (ushort)byteContentLength.Length; - httpResponse.Response_V1.Headers.UnknownHeaderCount = 0; + pinnedHeaders = new List(); - statusCode = - UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse( - _requestQueueHandle, - requestId, - 0, - &httpResponse, - null, - &dataWritten, - SafeLocalFree.Zero, - 0, - SafeNativeOverlapped.Zero, - IntPtr.Zero); + UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null; + knownHeaderInfo = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[1]; + gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned); + pinnedHeaders.Add(gcHandle); + httpResponse.pResponseInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject(); + + knownHeaderInfo[httpResponse.ResponseInfoCount].Type = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders; + knownHeaderInfo[httpResponse.ResponseInfoCount].Length = +#if NET45 + (uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS)); +#else + (uint)Marshal.SizeOf(); +#endif + + UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS(); + + header.HeaderId = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderWwwAuthenticate; + header.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // The docs say this is for www-auth only. + + UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[authChallenges.Count]; + gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned); + pinnedHeaders.Add(gcHandle); + header.KnownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject(); + + for (int headerValueIndex = 0; headerValueIndex < authChallenges.Count; headerValueIndex++) + { + // Add Value + string headerValue = authChallenges[headerValueIndex]; + byte[] bytes = new byte[HeaderEncoding.GetByteCount(headerValue)]; + nativeHeaderValues[header.KnownHeaderCount].RawValueLength = (ushort)bytes.Length; + HeaderEncoding.GetBytes(headerValue, 0, bytes.Length, bytes, 0); + gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + pinnedHeaders.Add(gcHandle); + nativeHeaderValues[header.KnownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject(); + header.KnownHeaderCount++; + } + + // This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set. + gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned); + pinnedHeaders.Add(gcHandle); + knownHeaderInfo[0].pInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject(); + + httpResponse.ResponseInfoCount = 1; + } + + httpResponse.Response_V1.StatusCode = (ushort)httpStatusCode; + string statusDescription = HttpReasonPhrase.Get(httpStatusCode); + uint dataWritten = 0; + uint statusCode; + byte[] byteReason = HeaderEncoding.GetBytes(statusDescription); + fixed (byte* pReason = byteReason) + { + httpResponse.Response_V1.pReason = (sbyte*)pReason; + httpResponse.Response_V1.ReasonLength = (ushort)byteReason.Length; + + byte[] byteContentLength = new byte[] { (byte)'0' }; + fixed (byte* pContentLength = byteContentLength) + { + (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].pRawValue = (sbyte*)pContentLength; + (&httpResponse.Response_V1.Headers.KnownHeaders)[(int)HttpSysResponseHeader.ContentLength].RawValueLength = (ushort)byteContentLength.Length; + httpResponse.Response_V1.Headers.UnknownHeaderCount = 0; + + statusCode = + UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse( + _requestQueueHandle, + requestId, + 0, + &httpResponse, + null, + &dataWritten, + SafeLocalFree.Zero, + 0, + SafeNativeOverlapped.Zero, + IntPtr.Zero); + } + } + if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + { + // if we fail to send a 401 something's seriously wrong, abort the request + RequestContext.CancelRequest(_requestQueueHandle, requestId); } } - if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) + finally { - // if we fail to send a 401 something's seriously wrong, abort the request - RequestContext.CancelRequest(_requestQueueHandle, requestId); + if (pinnedHeaders != null) + { + foreach (GCHandle handle in pinnedHeaders) + { + if (handle.IsAllocated) + { + handle.Free(); + } + } + } } } diff --git a/src/Microsoft.Net.Server/project.json b/src/Microsoft.Net.Server/project.json index eeff352bdf..ef68434d86 100644 --- a/src/Microsoft.Net.Server/project.json +++ b/src/Microsoft.Net.Server/project.json @@ -29,6 +29,7 @@ "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.Handles": "4.0.0.0", "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Claims": "0.1-alpha-*", "System.Security.Cryptography.X509Certificates": "4.0.0.0", "System.Security.Principal": "4.0.0.0", "System.Text.Encoding": "4.0.20.0", diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs index e6acd0f358..eae1a1e89e 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/AuthenticationTests.cs @@ -31,35 +31,61 @@ namespace Microsoft.AspNet.Server.WebListener private const string Address = "http://localhost:8080/"; [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] - [InlineData(AuthenticationType.Digest)] - [InlineData(AuthenticationType.Basic)] - [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | AuthenticationType.Digest | AuthenticationType.Basic)] - public async Task AuthTypes_EnabledButNotChalleneged_PassThrough(AuthenticationType authType) + [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_AllowAnonymous_NoChallenge(AuthenticationTypes authType) { - using (Utilities.CreateAuthServer(authType, env => + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); return Task.FromResult(0); })) { var response = await SendRequestAsync(Address); - response.EnsureSuccessStatusCode(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); } } [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] - // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented - [InlineData(AuthenticationType.Basic)] - public async Task AuthType_Specify401_ChallengesAdded(AuthenticationType authType) + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.Ntlm)] + // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationTypes authType) { using (Utilities.CreateAuthServer(authType, env => { - new DefaultHttpContext((IFeatureCollection)env).Response.StatusCode = 401; + throw new NotImplementedException(); + })) + { + var response = await SendRequestAsync(Address); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.Ntlm)] + // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationTypes authType) + { + 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.StatusCode = 401; return Task.FromResult(0); })) { @@ -70,17 +96,21 @@ namespace Microsoft.AspNet.Server.WebListener } [Fact] - public async Task MultipleAuthTypes_Specify401_ChallengesAdded() + public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded() { using (Utilities.CreateAuthServer( - AuthenticationType.Kerberos - | AuthenticationType.Negotiate - | AuthenticationType.Ntlm - /* | AuthenticationType.Digest TODO: Not implemented */ - | AuthenticationType.Basic, + AuthenticationTypes.Kerberos + | AuthenticationTypes.Negotiate + | AuthenticationTypes.Ntlm + /* | AuthenticationTypes.Digest TODO: Not implemented */ + | AuthenticationTypes.Basic + | AuthenticationTypes.AllowAnonymous, env => { - new DefaultHttpContext((IFeatureCollection)env).Response.StatusCode = 401; + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.StatusCode = 401; return Task.FromResult(0); })) { @@ -89,35 +119,64 @@ namespace Microsoft.AspNet.Server.WebListener Assert.Equal("Kerberos, Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); } } - /* TODO: User + [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] - // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented - // [InlineData(AuthenticationType.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | / *AuthenticationType.Digest |* / AuthenticationType.Basic)] - public async Task AuthTypes_Login_Success(AuthenticationType authType) + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [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)] + public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationTypes authType) { - int requestCount = 0; - using (Utilities.CreateAuthServer(authType, env => + int requestId = 0; + using (Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous, env => { - requestCount++; - / * // TODO: Expose user as feature. - object obj; - if (env.TryGetValue("server.User", out obj) && obj != null) + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + if (requestId == 0) { - return Task.FromResult(0); - }* / - new DefaultHttpContext((IFeatureCollection)env).Response.StatusCode = 401; + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.StatusCode = 401; + } + else if (requestId == 1) + { + Assert.True(context.User.Identity.IsAuthenticated); + } + else + { + throw new NotImplementedException(); + } + requestId++; return Task.FromResult(0); })) { var response = await SendRequestAsync(Address, useDefaultCredentials: true); - response.EnsureSuccessStatusCode(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [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)] + public async Task AuthTypes_RequireAuth_Success(AuthenticationTypes authType) + { + using (Utilities.CreateAuthServer(authType, env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + return Task.FromResult(0); + })) + { + var response = await SendRequestAsync(Address, useDefaultCredentials: true); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } - */ private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) { diff --git a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Utilities.cs b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Utilities.cs index 1a834b99cd..8281848d57 100644 --- a/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Utilities.cs +++ b/test/Microsoft.AspNet.Server.WebListener.FunctionalTests/Utilities.cs @@ -35,17 +35,17 @@ namespace Microsoft.AspNet.Server.WebListener return CreateServer("https", "localhost", "9090", string.Empty, app); } - internal static IDisposable CreateAuthServer(AuthenticationType authType, AppFunc app) + internal static IDisposable CreateAuthServer(AuthenticationTypes authType, AppFunc app) { return CreateServer("http", "localhost", "8080", string.Empty, authType, app); } internal static IDisposable CreateServer(string scheme, string host, string port, string path, AppFunc app) { - return CreateServer(scheme, host, port, path, AuthenticationType.None, app); + return CreateServer(scheme, host, port, path, AuthenticationTypes.AllowAnonymous, app); } - internal static IDisposable CreateServer(string scheme, string host, string port, string path, AuthenticationType authType, AppFunc app) + internal static IDisposable CreateServer(string scheme, string host, string port, string path, AuthenticationTypes authType, AppFunc app) { var factory = new ServerFactory(loggerFactory: null); var serverInfo = (ServerInformation)factory.Initialize(configuration: null); diff --git a/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs index 593575806c..ce57a86d35 100644 --- a/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs +++ b/test/Microsoft.Net.Server.FunctionalTests/AuthenticationTests.cs @@ -13,39 +13,65 @@ namespace Microsoft.Net.Server private const string Address = "http://localhost:8080/"; [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] - [InlineData(AuthenticationType.Digest)] - [InlineData(AuthenticationType.Basic)] - [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | AuthenticationType.Digest | AuthenticationType.Basic)] - public async Task AuthTypes_EnabledButNotChalleneged_PassThrough(AuthenticationType authType) + [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_AllowAnonymous_NoChallenge(AuthenticationTypes authType) { - using (var server = Utilities.CreateAuthServer(authType)) + 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); context.Dispose(); var response = await responseTask; - response.EnsureSuccessStatusCode(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, response.Headers.WwwAuthenticate.Count); } } [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.Ntlm)] // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented - [InlineData(AuthenticationType.Basic)] - public async Task AuthType_Specify401_ChallengesAdded(AuthenticationType authType) + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthType_RequireAuth_ChallengesAdded(AuthenticationTypes authType) { using (var server = Utilities.CreateAuthServer(authType)) { Task responseTask = SendRequestAsync(Address); + var contextTask = server.GetContextAsync(); // Fails when the server shuts down, the challenge happens internally. + + var response = await responseTask; + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(authType.ToString(), response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [InlineData(AuthenticationTypes.Ntlm)] + // [InlineData(AuthenticationTypes.Digest)] // TODO: Not implemented + [InlineData(AuthenticationTypes.Basic)] + public async Task AuthType_AllowAnonymousButSpecify401_ChallengesAdded(AuthenticationTypes authType) + { + 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); context.Response.StatusCode = 401; context.Dispose(); @@ -56,18 +82,21 @@ namespace Microsoft.Net.Server } [Fact] - public async Task MultipleAuthTypes_Specify401_ChallengesAdded() + public async Task MultipleAuthTypes_AllowAnonymousButSpecify401_ChallengesAdded() { using (var server = Utilities.CreateAuthServer( - AuthenticationType.Kerberos - | AuthenticationType.Negotiate - | AuthenticationType.Ntlm - /* | AuthenticationType.Digest TODO: Not implemented */ - | AuthenticationType.Basic)) + AuthenticationTypes.Kerberos + | AuthenticationTypes.Negotiate + | AuthenticationTypes.Ntlm + /* | AuthenticationTypes.Digest TODO: Not implemented */ + | AuthenticationTypes.Basic + | AuthenticationTypes.AllowAnonymous)) { Task responseTask = SendRequestAsync(Address); var context = await server.GetContextAsync(); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); context.Response.StatusCode = 401; context.Dispose(); @@ -76,35 +105,58 @@ namespace Microsoft.Net.Server Assert.Equal("Kerberos, Negotiate, NTLM, basic", response.Headers.WwwAuthenticate.ToString(), StringComparer.OrdinalIgnoreCase); } } - /* TODO: User + [Theory] - [InlineData(AuthenticationType.Kerberos)] - [InlineData(AuthenticationType.Negotiate)] - [InlineData(AuthenticationType.Ntlm)] - // [InlineData(AuthenticationType.Digest)] // TODO: Not implemented - // [InlineData(AuthenticationType.Basic)] // Doesn't work with default creds - [InlineData(AuthenticationType.Kerberos | AuthenticationType.Negotiate | AuthenticationType.Ntlm | / *AuthenticationType.Digest |* / AuthenticationType.Basic)] - public async Task AuthTypes_Login_Success(AuthenticationType authType) + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [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)] + public async Task AuthTypes_AllowAnonymousButSpecify401_Success(AuthenticationTypes authType) { - int requestCount = 0; - using (Utilities.CreateAuthServer(authType, env => + using (var server = Utilities.CreateAuthServer(authType | AuthenticationTypes.AllowAnonymous)) { - requestCount++; - / * // TODO: Expose user as feature. - object obj; - if (env.TryGetValue("server.User", out obj) && obj != null) - { - return Task.FromResult(0); - }* / - new DefaultHttpContext((IFeatureCollection)env).Response.StatusCode = 401; - return Task.FromResult(0); - })) - { - var response = await SendRequestAsync(Address, useDefaultCredentials: true); - response.EnsureSuccessStatusCode(); + Task responseTask = SendRequestAsync(Address, useDefaultCredentials: true); + + var context = await server.GetContextAsync(); + Assert.NotNull(context.User); + Assert.False(context.User.Identity.IsAuthenticated); + context.Response.StatusCode = 401; + context.Dispose(); + + context = await server.GetContextAsync(); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + [Theory] + [InlineData(AuthenticationTypes.Kerberos)] + [InlineData(AuthenticationTypes.Negotiate)] + [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)] + public async Task AuthTypes_RequireAuth_Success(AuthenticationTypes authType) + { + using (var server = Utilities.CreateAuthServer(authType)) + { + Task responseTask = SendRequestAsync(Address, useDefaultCredentials: true); + + var context = await server.GetContextAsync(); + Assert.NotNull(context.User); + Assert.True(context.User.Identity.IsAuthenticated); + context.Dispose(); + + var response = await responseTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } - */ private async Task SendRequestAsync(string uri, bool useDefaultCredentials = false) { diff --git a/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs b/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs index c11b7596f1..e911f5baa2 100644 --- a/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs +++ b/test/Microsoft.Net.Server.FunctionalTests/Utilities.cs @@ -16,17 +16,17 @@ namespace Microsoft.Net.Server return CreateServer("https", "localhost", "9090", string.Empty); } - internal static WebListener CreateAuthServer(AuthenticationType authType) + internal static WebListener CreateAuthServer(AuthenticationTypes authType) { return CreateServer("http", "localhost", "8080", string.Empty, authType); } internal static WebListener CreateServer(string scheme, string host, string port, string path) { - return CreateServer(scheme, host, port, path, AuthenticationType.None); + return CreateServer(scheme, host, port, path, AuthenticationTypes.AllowAnonymous); } - internal static WebListener CreateServer(string scheme, string host, string port, string path, AuthenticationType authType) + internal static WebListener CreateServer(string scheme, string host, string port, string path, AuthenticationTypes authType) { WebListener listener = new WebListener(); listener.UrlPrefixes.Add(UrlPrefix.Create(scheme, host, port, path));