Search code examples
c#wcfauthenticationssl-certificatewebhttpbinding

WCF Rest self hosted certificate secured service returns 401 unauthorized


I want to create a WCF console self hosted - server side authentication with Certificate - rest service.

I encountered a problem with actually invoking the Service, since i always get in the response 401 Unauthorized.

Since this is a "One-way" authentication, where the service is identifying itself to the client, why would i always get a 401 unautorized response as a client application (as if the client needs to identify itself to the service to access its resources?)

Could anyone help me with pinpointig where i am going wrong, and how to get my client-service communication working finally?

The Simple Service contract:

[ServiceContract]
public interface IService1
{

    [OperationContract]
    [WebGet(UriTemplate = "Students", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
    List<Student> GetStudentDetails();

    // TODO: Add GetMethod with parameter 
    [OperationContract]
    [WebGet(UriTemplate = "Student/{id}", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
    Student GetStudentWithId(string id);

    //TODO: add one post method here
    [OperationContract]
    [WebInvoke(Method="POST", UriTemplate = "Student/New", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
    void NewStudent(Stream stream);

    //TODO: add one post method here
    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "Student/NewS", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
    void NewStudentS(Student stream);
}

// Use a data contract as illustrated in the sample below to add composite types to service operations.
[DataContract]
public class Student
{
    [DataMember]
    public int ID
    {
        get;
        set;
    }

    [DataMember]
    public string Name
    {
        get;
        set;
    }
}

Service Implementation:

public class Service1 : IService1
{
    public List<Student> GetStudentDetails()
    {
        return new List<Student>() { new Student() { ID = 1, Name = "Goran" } };
    }

    public Student GetStudentWithId(string id)
    {
        return new Student() { ID = Int32.Parse(id), Name = "Ticbra RanGo" };
    }

    public void NewStudent(Stream stream)
    {
        using(stream)
        {
            // convert Stream Data to StreamReader
            StreamReader reader = new StreamReader(stream);
            var dataString = reader.ReadToEnd();

            Console.WriteLine(dataString);
        }
    }

    public void NewStudentS(Student student)
    {
        Console.WriteLine(student.Name);
    }
}

My console application running the service:

static void Main(string[] args)
    {
        Uri httpUrl = new Uri("https://localhost:8080/TestService");
        using (WebServiceHost host = new WebServiceHost(typeof(Service1)))
        {
             // Create the binding.  
            WSHttpBinding binding = new WSHttpBinding();
            binding.Name = "binding1";
            binding.Security.Mode = SecurityMode.Transport;


            host.AddServiceEndpoint(typeof(IService1), binding, httpUrl/*"rest"*/);
            // Enable metadata publishing.
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);              

            //Add host certificate to service wcf for identification
            host.Credentials.ServiceCertificate.SetCertificate(
                StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindBySubjectName,
                "localhost");

            host.Open();

            foreach (ServiceEndpoint se in host.Description.Endpoints)
                Console.WriteLine("Service is host with endpoint " + se.Address);

            Console.WriteLine("Host is running... Press < Enter > key to stop");
            Console.ReadLine();
            host.Close();
        }

        //Console.WriteLine("ASP.Net : " + ServiceHostingEnvironment.AspNetCompatibilityEnabled);
        Console.WriteLine("Host is running... Press < Enter > key to stop");
        Console.ReadLine();
    }

Note that i create the certificate root and children through KeyStore Explorer, and placed them approriately in personal and trusted root certificates on windows. Certificates

I mapped the server certificate to the port 8080 though CMD.

The clients i was using are SOAPUI, and my manual coded client. Client Code:

        WebRequest request = HttpWebRequest.Create(urlTextBox.Text);

        var webResponse = request.GetResponse();

        using (Stream dataStream = webResponse.GetResponseStream())
        {
            // Open the stream using a StreamReader for easy access.  
            StreamReader reader = new StreamReader(dataStream);
            // Read the content.  
            string responseFromServer = reader.ReadToEnd();
            // Display the content.  
            Console.WriteLine(responseFromServer);
            HttpResonseTextBox.Text = responseFromServer;
        }

Best regards and thank you in advance so much


Solution

  • WSHttpBinding binding = new WSHttpBinding();
    binding.Name = "binding1";
    binding.Security.Mode = SecurityMode.Transport;

    The above code takes the windows authentication as the way to authenticate the client.

            //this is the default value unless we specify it manually.
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;
    

    Therefore, we should call it on the client-side by providing a windows credential. Besides, this kind of WCF service is not called Rest API, it is called SOAP web service. We usually invoke it by using a client proxy.
    https://learn.microsoft.com/en-us/dotnet/framework/wcf/accessing-services-using-a-wcf-client
    Then setting up windows credential and call the method.

                ServiceReference1.ServiceClient client = new ServiceReference1.ServiceClient();
                //these are windows accounts on the server-side.
                client.ClientCredentials.Windows.ClientCredential.UserName = "administrator";
                client.ClientCredentials.Windows.ClientCredential.Password = "123456";
                var result = client.Test();
                Console.WriteLine(result);
    

    If we want to create a rest service, please take the Webhttpbinding to create the service.

                WebHttpBinding binding = new WebHttpBinding();
                binding.Security.Mode = WebHttpSecurityMode.Transport;
                //this is default value. 
                binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
    

    Likewise, we need to bind a certificate to the particular port in order to make the transport layer security available.

    netsh http add sslcert ipport=0.0.0.0:8000 certhash=0000000000003ed9cd0c315bbb6dc1c08da5e6 appid={00112233-4455-6677-8899-AABBCCDDEEFF}

    Netsh Http command.
    https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-configure-a-port-with-an-ssl-certificate
    Due to that, the security mode of authenticating the client is HttpClientCredentialType.None. We needn’t provide windows credentials on the client-side.
    Feel free to let me know if there is anything I can help with.