Skip to content

Commit

Permalink
Merge pull request #1371 from mconnew/Issue1111
Browse files Browse the repository at this point in the history
Enable basic auth tests with domainless custom authenticator
Fixes #1111
  • Loading branch information
mconnew authored Jul 27, 2016
2 parents 93bf4d5 + c96877a commit fa9874f
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// 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.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using System.Text;
using Xunit;
Expand All @@ -14,6 +14,8 @@ 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()
{
Expand All @@ -24,20 +26,18 @@ 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()
{
#if FULLXUNIT_NOTSUPPORTED
bool root_Certificate_Installed = Root_Certificate_Installed();
bool basic_Authentication_Available = Basic_Authentication_Available();
if (!root_Certificate_Installed || !basic_Authentication_Available)
if (!root_Certificate_Installed)
{
Console.WriteLine("---- Test SKIPPED --------------");
Console.WriteLine("Attempting to run the test in ToF, a ConditionalFact evaluated as FALSE.");
Console.WriteLine("Root_Certificate_Installed evaluated as {0}", root_Certificate_Installed);
Console.WriteLine("Basic_Authentication_Available evaluated as {0}", basic_Authentication_Available);
return;
}
#endif
Expand All @@ -49,13 +49,34 @@ public static void BasicAuthentication_RoundTrips_Echo()
basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

ChannelFactory<IWcfCustomUserNameService> factory = new ChannelFactory<IWcfCustomUserNameService>(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)
Expand All @@ -76,20 +97,18 @@ public static void BasicAuthentication_RoundTrips_Echo()
#if FULLXUNIT_NOTSUPPORTED
[Fact]
#else
[ConditionalFact(nameof(Root_Certificate_Installed), nameof(Basic_Authentication_Available))]
[ConditionalFact(nameof(Root_Certificate_Installed))]
#endif
[OuterLoop]
public static void BasicAuthenticationInvalidPwd_throw_MessageSecurityException()
{
#if FULLXUNIT_NOTSUPPORTED
bool root_Certificate_Installed = Root_Certificate_Installed();
bool basic_Authentication_Available = Basic_Authentication_Available();
if (!root_Certificate_Installed || !basic_Authentication_Available)
if (!root_Certificate_Installed)
{
Console.WriteLine("---- Test SKIPPED --------------");
Console.WriteLine("Attempting to run the test in ToF, a ConditionalFact evaluated as FALSE.");
Console.WriteLine("Root_Certificate_Installed evaluated as {0}", root_Certificate_Installed);
Console.WriteLine("Basic_Authentication_Available evaluated as {0}", basic_Authentication_Available);
return;
}
#endif
Expand All @@ -106,13 +125,31 @@ public static void BasicAuthenticationInvalidPwd_throw_MessageSecurityException(
basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

ChannelFactory<IWcfCustomUserNameService> factory = new ChannelFactory<IWcfCustomUserNameService>(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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IIdentity>(1);
operationContext.ServiceSecurityContext.AuthorizationContext.Properties.Add("Identities", identitiesListObject);
}

var identities = identitiesListObject as IList<IIdentity>;
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<ServiceEndpoint> 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();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading

0 comments on commit fa9874f

Please sign in to comment.