0

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);
    }
wickd
  • 305
  • 3
  • 13
  • Did you debug your solution. Do you get into Validate method inside your custom **UserNamePasswordValidator** or not? – Mimas Oct 22 '15 at 09:47
  • I did debug and set the appropriate breakpoints, but it does not seem to step into the custom validate method. I then thought it has something to do with the IIS express basic authentication setting and turned it off, but this does not solve the problem... – wickd Oct 22 '15 at 11:52
  • I really don't have direct answer to the question, but if I were you I would started step by step. First I would change the binding to wsHttpBinding and tried to reproduce that in code (taking this example https://msdn.microsoft.com/en-us/library/ms733775.aspx) and using C#-written client. If it works, that would mean that your validator at least recognized and applied. And I would not go further unit this simple MSDN sample works – Mimas Oct 22 '15 at 12:19
  • If that would not work, maybe the problem is deeper. I guess you have read that (http://stackoverflow.com/questions/18123082/wcf-basic-transport-security-issue-when-hosted-in-iis) – Mimas Oct 22 '15 at 12:21
  • I used this just now and came to exactly the same dead end: http://www.allenconway.net/2012/06/restful-services-authenticating-clients.html – wickd Oct 22 '15 at 13:04
  • I think this is the solution: http://www.allenconway.net/2012/07/using-basic-authentication-in-rest.html. So basically IIS intercepts the basic authentication and does not pass this to the custom validator... – wickd Oct 22 '15 at 13:10
  • So actually this is exactly what I pointed you to in my last link (http://stackoverflow.com/questions/18123082/wcf-basic-transport-security-issue-when-hosted-in-iis). Creation of custom authorization manager. Hope this will help. – Mimas Oct 22 '15 at 13:47
  • The `` section of the web.config - where you declare userNamePasswordValidationMode and the class of your authenticator - appears to be commented out. – stuartd Oct 22 '15 at 16:46
  • @wickd if you are using the ServiceAuthorizationManager as Allen has mentioned in his blog then you have to remove couple of settings from your configuration like - ` `. The ServiceAuthorizationManager will take care of it automatically. See my answer yesterday I wrote a [complete step by step guide](http://www.c-sharpcorner.com/UploadFile/vendettamit/create-secure-wcf-rest-api-with-custom-basic-authentication) to create and secure WCF Rest service using custom BasicAuthentication and securing communication with SSL. – vendettamit Oct 22 '15 at 18:02
  • Also make sure you have 'Anonymous Authentication' enabled for the website. – vendettamit Oct 22 '15 at 18:03

1 Answers1

2

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.

vendettamit
  • 14,315
  • 2
  • 32
  • 54