diff --git a/src/System.Private.ServiceModel/tests/Common/Scenarios/Endpoints.cs b/src/System.Private.ServiceModel/tests/Common/Scenarios/Endpoints.cs index 64d393699169..a4dd6c7adad8 100644 --- a/src/System.Private.ServiceModel/tests/Common/Scenarios/Endpoints.cs +++ b/src/System.Private.ServiceModel/tests/Common/Scenarios/Endpoints.cs @@ -168,7 +168,7 @@ public static string Https_BasicAuth_Address { get { - return GetEndpointAddress("BasicAuthentication/BasicAuth.svc/https-basic", protocol: "https"); + return GetEndpointAddress("BasicAuth.svc/https-basic", protocol: "https"); } } diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/ClientCredentialTypeTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/ClientCredentialTypeTests.cs index 193e6c788fdf..32a7bf116bfa 100644 --- a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/ClientCredentialTypeTests.cs +++ b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/ClientCredentialTypeTests.cs @@ -9,11 +9,14 @@ using System.Text; using Xunit; using Infrastructure.Common; +using System.ServiceModel.Channels; public class Https_ClientCredentialTypeTests : ConditionalWcfTest { private static string s_username; private static string s_password; + private const string BasicUsernameHeaderName = "BasicUsername"; + private const string BasicPasswordHeaderName = "BasicPassword"; static Https_ClientCredentialTypeTests() { @@ -24,7 +27,7 @@ static Https_ClientCredentialTypeTests() #if FULLXUNIT_NOTSUPPORTED [Fact] #else - [ConditionalFact(nameof(Root_Certificate_Installed), nameof(Basic_Authentication_Available))] + [ConditionalFact(nameof(Root_Certificate_Installed))] #endif [OuterLoop] public static void BasicAuthentication_RoundTrips_Echo() @@ -49,13 +52,34 @@ public static void BasicAuthentication_RoundTrips_Echo() basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; ChannelFactory factory = new ChannelFactory(basicHttpBinding, new EndpointAddress(Endpoints.Https_BasicAuth_Address)); - factory.Credentials.UserName.UserName = "test1"; - factory.Credentials.UserName.Password = "Mytestpwd1"; + string username = Guid.NewGuid().ToString("n").Substring(0, 8); + string password = Guid.NewGuid().ToString("n").Substring(0, 16); + factory.Credentials.UserName.UserName = username; + factory.Credentials.UserName.Password = password; IWcfCustomUserNameService serviceProxy = factory.CreateChannel(); string testString = "I am a test"; - string result = serviceProxy.Echo(testString); + string result; + using (var scope = new OperationContextScope((IContextChannel)serviceProxy)) + { + HttpRequestMessageProperty requestMessageProperty; + if (!OperationContext.Current.OutgoingMessageProperties.ContainsKey(HttpRequestMessageProperty.Name)) + { + requestMessageProperty = new HttpRequestMessageProperty(); + OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessageProperty; + } + else + { + requestMessageProperty = (HttpRequestMessageProperty)OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name]; + } + + requestMessageProperty.Headers[BasicUsernameHeaderName] = username; + requestMessageProperty.Headers[BasicPasswordHeaderName] = password; + + result = serviceProxy.Echo(testString); + } + bool success = string.Equals(result, testString); if (!success) @@ -106,13 +130,31 @@ public static void BasicAuthenticationInvalidPwd_throw_MessageSecurityException( basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; ChannelFactory factory = new ChannelFactory(basicHttpBinding, new EndpointAddress(Endpoints.Https_BasicAuth_Address)); - factory.Credentials.UserName.UserName = "test1"; - factory.Credentials.UserName.Password = "test1"; + string username = Guid.NewGuid().ToString("n").Substring(0, 8); + string password = Guid.NewGuid().ToString("n").Substring(0, 16); + factory.Credentials.UserName.UserName = username; + factory.Credentials.UserName.Password = password + "Invalid"; IWcfCustomUserNameService serviceProxy = factory.CreateChannel(); string testString = "I am a test"; - string result = serviceProxy.Echo(testString); + using (var scope = new OperationContextScope((IContextChannel)serviceProxy)) + { + HttpRequestMessageProperty requestMessageProperty; + if (!OperationContext.Current.OutgoingMessageProperties.ContainsKey(HttpRequestMessageProperty.Name)) + { + requestMessageProperty = new HttpRequestMessageProperty(); + OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = requestMessageProperty; + } + else + { + requestMessageProperty = (HttpRequestMessageProperty)OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name]; + } + + requestMessageProperty.Headers[BasicUsernameHeaderName] = username; + requestMessageProperty.Headers[BasicPasswordHeaderName] = password; + string result = serviceProxy.Echo(testString); + } }); Assert.True(exception.Message.ToLower().Contains(message), string.Format("Expected exception message to contain: '{0}', actual message is: '{1}'", message, exception.Message)); diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/BasicServiceAuthorizationManager.cs b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/BasicServiceAuthorizationManager.cs new file mode 100644 index 000000000000..6b69d0810b51 --- /dev/null +++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/BasicServiceAuthorizationManager.cs @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Security.Cryptography; +using System.Security.Principal; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.ServiceModel.Description; +using System.ServiceModel.Dispatcher; +using System.Text; + +namespace WcfService +{ + public abstract class BasicServiceAuthorizationManager : ServiceAuthorizationManager, IServiceBehavior + { + private readonly string _realm; + + public BasicServiceAuthorizationManager(string realm) + { + if (realm == null) + { + realm = string.Empty; + } + + _realm = realm; + } + + private bool Authorized(BasicAuthenticationState basicState, OperationContext operationContext, ref Message message) + { + object identitiesListObject; + if (!operationContext.ServiceSecurityContext.AuthorizationContext.Properties.TryGetValue("Identities", + out identitiesListObject)) + { + identitiesListObject = new List(1); + operationContext.ServiceSecurityContext.AuthorizationContext.Properties.Add("Identities", identitiesListObject); + } + + var identities = identitiesListObject as IList; + identities.Add(new GenericIdentity(basicState.Username, "GenericPrincipal")); + + return true; + } + + public override bool CheckAccess(OperationContext operationContext, ref Message message) + { + var basicState = new BasicAuthenticationState(operationContext, GetRealm(ref message)); + if (!basicState.IsRequestBasicAuth) + { + return UnauthorizedResponse(basicState); + } + + string password; + if (!GetPassword(ref message, basicState.Username, out password)) + { + return UnauthorizedResponse(basicState); + } + + if(basicState.Password != password) + { + // According to RFC2616, a forbidden response should be in response to valid credentials where the + // authenticated user is not allowed to use the site but WCF responds with Forbbiden with an incorrect + // password. We should be returning Unauthorized, but this matches WCF behavior. + return ForbiddenResponse(basicState); + } + + return Authorized(basicState, operationContext, ref message); + } + + public virtual string GetRealm(ref Message message) + { + return _realm; + } + + public abstract bool GetPassword(ref Message message, string username, out string password); + + private bool UnauthorizedResponse(BasicAuthenticationState basicState) + { + basicState.SetChallengeResponse(HttpStatusCode.Unauthorized, "Access Denied"); + return false; + } + + private bool ForbiddenResponse(BasicAuthenticationState basicState) + { + basicState.SetChallengeResponse(HttpStatusCode.Forbidden, "Access Denied"); + return false; + } + + #region IServiceBehavior + public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } + + public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection endpoints, + BindingParameterCollection bindingParameters) + { } + + public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) + { + serviceHostBase.Authorization.ServiceAuthorizationManager = this; + foreach (ChannelDispatcherBase t in serviceHostBase.ChannelDispatchers) + { + ChannelDispatcher channelDispatcher = t as ChannelDispatcher; + foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints) + { + endpointDispatcher.DispatchRuntime.ServiceAuthorizationManager = this; + } + } + } + #endregion + + private struct BasicAuthenticationState + { + private const string BasicAuthenticationMechanism = "Basic "; + private const int BasicAuthenticationMechanismLength = 6; // DigestAuthenticationMechanism.Length; + private const string RealmAuthenticationParameter = "realm"; + private const string AuthenticationChallengeHeaderName = "WWW-Authenticate"; + private const string AuthorizationHeaderName = "Authorization"; + private readonly string _authorizationHeader; + private readonly OperationContext _operationContext; + private readonly string _realm; + private string _username; + private string _password; + private bool? _authorized; + + public BasicAuthenticationState(OperationContext operationContext, string realm) + { + _operationContext = operationContext; + _realm = realm; + _username = _password = string.Empty; + _authorized = new bool?(); + _authorizationHeader = GetAuthorizationHeader(operationContext); + if (_authorizationHeader.Length < BasicAuthenticationMechanismLength) + { + _authorized = false; + return; + } + + string authBase64Encoded = _authorizationHeader.Substring(BasicAuthenticationMechanismLength); + var authDecodedBytes = Convert.FromBase64String(authBase64Encoded); + var authDecoded = Encoding.UTF8.GetString(authDecodedBytes); + int colonPos = authDecoded.IndexOf(':'); + if(colonPos <= 0) + { + _authorized = false; + return; + } + _username = authDecoded.Substring(0, colonPos); + _password = authDecoded.Substring(colonPos + 1); + } + + public bool IsRequestBasicAuth { get { return _authorizationHeader.StartsWith(BasicAuthenticationMechanism); } } + + public string Username { get { return _username; } } + + public string Password { get { return _password; } } + + public void SetChallengeResponse(HttpStatusCode statusCode, string statusDescription) + { + StringBuilder authChallenge = new StringBuilder(BasicAuthenticationMechanism); + authChallenge.AppendFormat(RealmAuthenticationParameter + "=\"{0}\", ", _realm); + + object responsePropertyObject; + if (!_operationContext.OutgoingMessageProperties.TryGetValue(HttpResponseMessageProperty.Name, out responsePropertyObject)) + { + responsePropertyObject = new HttpResponseMessageProperty(); + _operationContext.OutgoingMessageProperties[HttpResponseMessageProperty.Name] = responsePropertyObject; + } + + var responseMessageProperty = (HttpResponseMessageProperty)responsePropertyObject; + responseMessageProperty.Headers[AuthenticationChallengeHeaderName] = authChallenge.ToString(); + responseMessageProperty.StatusCode = statusCode; + responseMessageProperty.StatusDescription = statusDescription; + } + + private static string GetAuthorizationHeader(OperationContext operationContext) + { + object requestMessagePropertyObject; + if (!operationContext.IncomingMessageProperties.TryGetValue(HttpRequestMessageProperty.Name, + out requestMessagePropertyObject)) + { + throw new InvalidOperationException("Not an HTTP request"); + } + + HttpRequestMessageProperty requestMessageProperties = (HttpRequestMessageProperty)requestMessagePropertyObject; + var requestHeaders = requestMessageProperties.Headers; + + var authorizationHeader = requestHeaders[AuthorizationHeaderName]; + if (authorizationHeader == null) + { + authorizationHeader = string.Empty; + } + + return authorizationHeader.Trim(); + } + } + } +} diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/AuthenticationResourceHelper.cs b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/AuthenticationResourceHelper.cs index ad872642ca8a..fbcd9be2c7e2 100644 --- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/AuthenticationResourceHelper.cs +++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/AuthenticationResourceHelper.cs @@ -18,6 +18,40 @@ public static void ConfigureServiceHostUseDigestAuth(ServiceHost serviceHost) serviceHost.Description.Behaviors.Add(authManager); } + public static void ConfigureServiceHostUseBasicAuth(ServiceHost serviceHost) + { + var authManager = new ResourceBasicServiceAuthorizationManager(); + serviceHost.Description.Behaviors.Add(authManager); + } + + private class ResourceBasicServiceAuthorizationManager : BasicServiceAuthorizationManager + { + private const string BasicUsernameHeaderName = "BasicUsername"; + private const string BasicPasswordHeaderName = "BasicPassword"; + + public ResourceBasicServiceAuthorizationManager() : base("NoRealm") { } + + public override bool GetPassword(ref Message message, string username, out string password) + { + if (!message.Properties.ContainsKey(HttpRequestMessageProperty.Name)) + { + password = null; + return false; + } + + var requestProperty = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name]; + string sentUsername = requestProperty.Headers.Get(BasicUsernameHeaderName); + if (username.Equals(sentUsername)) + { + password = requestProperty.Headers.Get(BasicPasswordHeaderName); + return true; + } + + password = null; + return false; + } + } + private class ResourceDigestServiceAuthorizationManager : DigestServiceAuthorizationManager { private const string DigestUsernameHeaderName = "DigestUsername"; diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/BasicAuthTestServiceHost.cs b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/BasicAuthTestServiceHost.cs index e47e3796e6ba..7808c79e97e5 100644 --- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/BasicAuthTestServiceHost.cs +++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/BasicAuthTestServiceHost.cs @@ -20,6 +20,7 @@ protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAdd return serviceHost; } } + public class BasicAuthTestServiceHost : TestServiceHostBase { protected override string Address { get { return "https-basic"; } } @@ -28,18 +29,9 @@ public class BasicAuthTestServiceHost : TestServiceHostBase(); - this.Description.Behaviors.Add(GetServiceCredentials()); + AuthenticationResourceHelper.ConfigureServiceHostUseBasicAuth(this); } } } diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/BasicAuthentication/web.config b/src/System.Private.ServiceModel/tools/IISHostedWcfService/BasicAuthentication/web.config deleted file mode 100644 index e3564bf2faa1..000000000000 --- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/BasicAuthentication/web.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/Web.config b/src/System.Private.ServiceModel/tools/IISHostedWcfService/Web.config index b9a8ac5a8507..3ce033290f4d 100644 --- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/Web.config +++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/Web.config @@ -22,7 +22,7 @@ - + diff --git a/src/System.Private.ServiceModel/tools/SelfHostedWcfService/Program.cs b/src/System.Private.ServiceModel/tools/SelfHostedWcfService/Program.cs index 21a29245174e..bf49480b3c35 100644 --- a/src/System.Private.ServiceModel/tools/SelfHostedWcfService/Program.cs +++ b/src/System.Private.ServiceModel/tools/SelfHostedWcfService/Program.cs @@ -37,7 +37,7 @@ private static void Main() string websocketBaseAddress = string.Format(@"http://localhost:{0}", s_websocketPort); string websocketsBaseAddress = string.Format(@"https://localhost:{0}", s_websocketsPort); - Uri[] basicAuthTestServiceHostbaseAddress = new Uri[] { new Uri(string.Format("{0}/BasicAuthentication/BasicAuth.svc", httpsBaseAddress)) }; + Uri[] basicAuthTestServiceHostbaseAddress = new Uri[] { new Uri(string.Format("{0}/BasicAuth.svc", httpsBaseAddress)) }; BasicAuthTestServiceHost basicAuthTestServiceHostServiceHost = new BasicAuthTestServiceHost(typeof(WcfService.WcfUserNameService), basicAuthTestServiceHostbaseAddress); basicAuthTestServiceHostServiceHost.Open();