Search code examples
c#wcfrestauthenticationwebhttpbinding

Cannot Get WCF Basic authentication to authenticate user


So at this stage after trying to get this working I am now resorting to asking you lot this question.

Yes I went through a bunch of stack overflow questions similar to mine, but to no avail.

All I want to do is add basic authentication over SSL. From the million and half thousand 44 thousand and 7 tutorials I paged through, this really seems to be a simple task.

All I get is a dialog box that pops up that asks me for a username and password, but even though I pass it the correct credentials it just pops up again and again.

I have also played around with the config settings in iis express. If i disable basic authentication I get an error complaining that this is not switched on.

I know that I am being blond, but before I put a hole in my screen here is my web.config

web.config

<?xml version="1.0"?>
<configuration>

  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5"/>
  </system.web>

  <!--DIAGNOSTICS-->
  <system.diagnostics>
    <trace autoflush="true"/>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true">
        <listeners>
          <add name="ServiceModel"
               type="System.Diagnostics.XmlWriterTraceListener"
               initializeData="C:\ServiceModel.svclog" />
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="MessageLogging"
               type="System.Diagnostics.XmlWriterTraceListener"
               initializeData="C:\MessageLogging.svclog" />
        </listeners>
      </source>
    </sources>
  </system.diagnostics>

  <system.serviceModel>

    <diagnostics>
      <messageLogging logEntireMessage="True"
                      logMalformedMessages="False"
                      logMessagesAtServiceLevel="True"
                      logMessagesAtTransportLevel="False"
                      maxMessagesToLog="10000"
                      maxSizeOfMessageToLog="10000" />
    </diagnostics>

    <bindings>

      <webHttpBinding>
        <binding name="SSSLayer">
          <security mode="Transport">
            <transport clientCredentialType="Basic"></transport>
          </security>
        </binding>
      </webHttpBinding>
    </bindings>

    <services>
      <service behaviorConfiguration="serviceBehaviour" name="Booky.Machine_SVC">
        <endpoint address="" 
                  behaviorConfiguration="RESTBehaviour" 
                  binding="webHttpBinding"
                  bindingConfiguration="SSSLayer"
                  contract="Booky.IMachine_SVC" />
      </service>
    </services>


    <behaviors>
      <endpointBehaviors>
        <behavior name="RESTBehaviour">
          <webHttp/>
        </behavior>                  
      </endpointBehaviors>

      <serviceBehaviors>
        <behavior name="serviceBehaviour">
          <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
          <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>

          <!--<serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Booky.Authentication, Booky" />
          </serviceCredentials>-->
        </behavior>
      </serviceBehaviors>
    </behaviors>


    <protocolMapping>
        <add binding="basicHttpsBinding" scheme="https" />
    </protocolMapping>    
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <!--
        To browse web app root directory during debugging, set the value below to true.
        Set to false before deployment to avoid disclosing web app folder information.
      -->
    <directoryBrowse enabled="true"/>
  </system.webServer>

</configuration>

Authentication Class

namespace Booky
{
    public class Authentication : System.IdentityModel.Selectors.UserNamePasswordValidator
    {
        public override void Validate(string UserName, string Password)
        {
            if (UserName == null || Password == null)
            {
                throw new ArgumentNullException("Username or Password is Incorrect");
            }

            if (!(UserName == "wickd" && Password == "OMIG2015"))
            {
                throw new Exception("Invalid Credentials");
            }
        }
    }
}

Machine_SVC

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple, IncludeExceptionDetailInFaults=true)]
    public class Machine_SVC : IMachine_SVC
    {
        /// <summary>
        /// Retrieves a serial number
        /// </summary>
        /// <param name="serial"></param>
        /// <returns></returns>
        Machine IMachine_SVC.GetMachine(string serial)
        {
            var res = Machine.GetMachine(serial);

            if (CheckContent(res))
                return res;
            else
                return null;
        }

        /// <summary>
        /// Creates a new machine object
        /// </summary>
        /// <param name="machine"></param>
        void IMachine_SVC.CreateMachine(Machine machine)
        {
            if (!Machine.CreateMachine(machine))
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.PreconditionFailed;
                WebOperationContext.Current.OutgoingResponse.StatusDescription = "A serial and status needs to be specified for the machine to be created";
            }
        }

        /// <summary>
        /// This will update the machine information
        /// </summary>
        /// <param name="machine"></param>
        /// <param name="serial"></param>
        void IMachine_SVC.UpdateMachineInfo(Machine machine, string serial)
        {
            var result = Machine.UpdateMachineInfo(machine, serial);

            CheckUpdateCreateResult(result);
        }

        private bool CheckContent(object result, HttpStatusCode code = HttpStatusCode.NotFound)
        {
            if (result != null)
            {
                return true;
            }
            else
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = code;
                return false;
            }
        }

        private void CheckUpdateCreateResult(ReturnCodes result)
        {
            if (result == ReturnCodes.DATASETINCOMPLETE)
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.PreconditionFailed;
                WebOperationContext.Current.OutgoingResponse.StatusDescription = "Not all the required attributes were provided. You need a serial, bitlocked, model and type attribute";
            }

            if (result == ReturnCodes.INVALIDDATA)
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.PreconditionFailed;
                WebOperationContext.Current.OutgoingResponse.StatusDescription = "The serial provided in the url is not the same as in the json object";
            }

            if (result == ReturnCodes.NOTEXIST)
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
                WebOperationContext.Current.OutgoingResponse.StatusDescription = "The serial you have provided does not exist yet! You need to create it first!";
            }
        }

        /// <summary>
        /// Retrieves a list of owners of the machine with the owners organized from last login first
        /// </summary>
        /// <param name="serial"></param>
        /// <returns></returns>
        List<MachineOwner> IMachine_SVC.GetMachineOwners(string serial)
        {
            var owners = MachineOwners.GetOwners(serial);

            if (CheckContent(owners))
                return owners;
            else
                return null;
        }

        /// <summary>
        /// Adds a new Machine owner. Only adds the serial, nothing else
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="serial"></param>
        void IMachine_SVC.AddMachineOwner(MachineOwner owner, string serial)
        {
            var result = MachineOwners.AddOwner(owner, serial);

            CheckUpdateCreateResult(result);
        }

        /// <summary>
        /// Retrieves the statuses for a particular machine
        /// </summary>
        /// <param name="serial"></param>
        /// <returns></returns>
        List<MachineStatus> IMachine_SVC.GetMachineStatuses(string serial)
        {
            var statuses = MachineStatus.GetStatusList(serial);

            if (CheckContent(statuses))
                return statuses;
            else
                return null;
        }

        /// <summary>
        /// This will update a machine status.
        ///     - Checks that the operation is valid compared to last machine login
        ///     - Checks that status is indeed valid
        /// </summary>
        /// <param name="serial"></param>
        /// <param name="status"></param>
        void IMachine_SVC.UpdateMachineStatus(string serial, MachineStatus status)
        {
            var result = MachineStatus.UpdateStatus(serial, status);

            CheckUpdateCreateResult(result);
        }

        /// <summary>
        /// Retrieves a list of all machines ever registered on the network
        /// </summary>
        /// <returns></returns>
        List<Machine> IMachine_SVC.GetAllMachines()
        {
            var machines = Machines.GetAllMachines();

            if (CheckContent(machines))
                return machines;
            else
                return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="search"></param>
        void IMachine_SVC.CreateMachineSearch(MachineSearch search)
        {
            throw new NotImplementedException();
        }
    }

IMachine_SVC

[ServiceContract]
    public interface IMachine_SVC
    {
        [OperationContract]
        [WebInvoke(Method="GET",
            RequestFormat=WebMessageFormat.Json,
            BodyStyle=WebMessageBodyStyle.Bare,
            ResponseFormat=WebMessageFormat.Json,
            UriTemplate="/machine/{serial}")]
        Machine GetMachine(string serial);

        [OperationContract]
        [WebInvoke(Method = "PUT",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine/{serial}")]
        void UpdateMachineInfo(Machine machine, string serial);

        [OperationContract]
        [WebInvoke(Method = "POST",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine")]
        void CreateMachine(Machine machine);

        [OperationContract]
        [WebInvoke(Method = "GET",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine/{serial}/owners")]
        List<MachineOwner> GetMachineOwners(string serial);

        [OperationContract]
        [WebInvoke(Method = "POST",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine/{serial}/owner")]
        void AddMachineOwner(MachineOwner owner, string serial);

        [OperationContract]
        [WebInvoke(Method = "GET",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine/{serial}/statuses")]
        List<MachineStatus> GetMachineStatuses(string serial);

        [OperationContract]
        [WebInvoke(Method = "PUT",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machine/{serial}/status")]
        void UpdateMachineStatus(string serial, MachineStatus status);

        [OperationContract]
        [WebInvoke(Method = "GET",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machines")]
        List<Machine> GetAllMachines();

        [OperationContract]
        [WebInvoke(Method = "POST",
            RequestFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Json,
            UriTemplate = "/machines")]
        void CreateMachineSearch(MachineSearch search);
    }

Solution

  • If you're hosting this service in IIS then you'll face problem because the Custom password validator was not built for IIS hosting. It will work perfectly fine with stand alone hosting. See details here where Phil clearly stated that Note that this is only supported under self hosted services.

    Instead of using Custom validator you can achieve it by extending "ServiceAuthorizationManager" class.

    public class RestAuthorizationManager: ServiceAuthorizationManager  
    {  
        protected override bool CheckAccessCore(OperationContext operationContext)
        {
            //Extract the Authorization header, and parse out the credentials converting the Base64 string:
            var authHeader = WebOperationContext.Current.IncomingRequest.Headers["Authorization"];
            if ((authHeader != null) && (authHeader != string.Empty))
            {
                var svcCredentials = System.Text.ASCIIEncoding.ASCII
                        .GetString(Convert.FromBase64String(authHeader.Substring(6)))
                        .Split(':');
                var user = new { Name = svcCredentials[0], Password = svcCredentials[1] };
                if ((user.Name == "user1" && user.Password == "test"))
                {
                    //User is authrized and originating call will proceed
                    return true;
                }
                else
                {
                    //not authorized
                    return false;
                }
            }
            else
            {
                //No authorization header was provided, so challenge the client to provide before proceeding:
                WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"MyWCFService\"");
                //Throw an exception with the associated HTTP status code equivalent to HTTP status 401
                throw new WebFaultException("Please provide a username and password", HttpStatusCode.Unauthorized);
            }
        }
    }
    

    Add RestAuthorizationManager to service behavior.

    <serviceBehaviors>  
      <behavior name="ServiceBehavior">  
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>  
        <serviceDebug includeExceptionDetailInFaults="true"/>  
        <serviceAuthorization   
          serviceAuthorizationManagerType  
            =" WcfWebHttpIISHostingSample.RestAuthorizationManager, WcfWebHttpIISHostingSample"/>  
      </behavior>  
    </serviceBehaviors>  
    

    This should get you going.

    I wrote a complete guide for creating and securing WCF REST service with Basic Authentication with SSL. You may want to go through it even the security behavior is also a part of an extension library of WCF REST and webhttp behaviors. See CustomAuthenticationBehavior details.