MichaelS
MichaelS

Reputation: 3831

Selfhosted WCF Service with SSL and Username and Password authentication

I'd like to publish a WCF service from a console application. For security purposes I want to communicate over SSL, so I created a self-signed certificate. For authentication I wrote my own UserNamePasswordValidator. Unfortunately this is not working

This is my code so far:

Server

public class Program
{
    public static void Main()
    {
        var baseAddress = new Uri("https://localhost:8080/SelfHostedUsernamePasswordService");

        using (var host = new ServiceHost(typeof(SelfHostedUsernamePasswordService), baseAddress))
        {
            var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential);
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
            binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;

            var endpoint = host.AddServiceEndpoint(typeof(ISelfHostedUsernamePasswordService), binding, baseAddress);

            var cf = new ChannelFactory<ISelfHostedUsernamePasswordService>(binding, endpoint.Address);
            cf.Credentials.ClientCertificate.SetCertificate(
                StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindByThumbprint,
                "0000000000000000000000000000000000000000");

            var metadataBehavior = new ServiceMetadataBehavior();
            metadataBehavior.HttpsGetEnabled = true;
            metadataBehavior.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
            host.Description.Behaviors.Add(metadataBehavior);

            var credentialBehavior = new ServiceCredentials();
            credentialBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new UsernamePasswordValidator();
            credentialBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
            host.Description.Behaviors.Add(credentialBehavior);

            host.Open();

            Console.WriteLine("The service is ready at {0}", baseAddress);
            Console.WriteLine("Press <Enter> to stop the service.");
            Console.ReadLine();

            host.Close();
        }
    }
}

public class UsernamePasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (!string.Equals(userName, "admin", StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(password, "password", StringComparison.Ordinal))
        {
            Console.WriteLine("Validation failed.");
            throw new SecurityTokenException("Validation failed.");
        }
        Console.WriteLine("Validation successful.");
    }
}

Client

class Program
{
    static void Main()
    {
        using (var client = new SelfHostedUsernamePasswordServiceClient())
        {
            client.ClientCredentials.UserName.UserName = "admin";
            client.ClientCredentials.UserName.Password = "password";

            var result = client.GetData(12345);
            Console.WriteLine("Result from service: {0}", result);

            client.Close();
        }
    }
}

With this code I get a MessageSecurityException (Cannot find a token authenticator for the 'System.IdentityModel.Tokens.UserNameSecurityToken' token type). But I think with creating a TokenAuthenticator I'm on the wrong path...

Btw, the UsernamePasswordValidator is never called.

Upvotes: 4

Views: 1838

Answers (2)

Lucio Pelinson
Lucio Pelinson

Reputation: 101

I have needed TCP/IP Protocol, after one week of hard work, this works fine. o:)
See the complete solution:

Server:

using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Text;

namespace Demo.Services
{
    public class TcpHostService
    {
        public const string CertificateName = "MyCertificateName";
        public static ServiceHost GetServiceHost()
        {
            string tcpHost = GetTcpHost();
            var portsharingBinding = new NetTcpBinding();
            portsharingBinding.Security.Mode = SecurityMode.TransportWithMessageCredential;
            portsharingBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            portsharingBinding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Certificate;

            var serviceHost = new ServiceHost(typeof(RemotingService), new Uri(tcpHost));
            serviceHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
            serviceHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNameValidator();
            serviceHost.AddServiceEndpoint(typeof(IRemote), portsharingBinding, tcpHost);

            if (!File.Exists("Certificate.pfx"))
            {
                MakeCert();
            }

            using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
            {
                store.Open(OpenFlags.ReadOnly);
                var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, CertificateName, false);
                if (certificates == null || certificates.Count == 0)
                {
                    InstallCert();
                }
            }

            serviceHost.Credentials.ServiceCertificate.SetCertificate(
                StoreLocation.CurrentUser, StoreName.My,
                X509FindType.FindBySubjectName, CertificateName);

            Console.WriteLine("Server escutando " + tcpHost);
            return serviceHost;
        }


        private static void MakeCert()
        {
            var rsa = RSA.Create(2048);
            var req = new CertificateRequest($"cn={CertificateName},OU=UserAccounts,DC=corp,DC=contoso,DC=com", 
                rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);

            var sanBuilder = new SubjectAlternativeNameBuilder();
            sanBuilder.AddIpAddress(IPAddress.Parse("127.0.0.1"));
            req.CertificateExtensions.Add(sanBuilder.Build());

            var oidCollection = new OidCollection
            {
                new Oid("1.3.6.1.5.5.7.3.2")
            };
            req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oidCollection, true));
            req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
            req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, false));

            using (X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.Now.AddDays(-10), DateTimeOffset.Now.AddYears(5)))
            {
                cert.FriendlyName = "JJConsulting Integration Certificate";

                // Create PFX (PKCS #12) with private key
                File.WriteAllBytes("Certificate.pfx", cert.Export(X509ContentType.Pfx, "pwd123"));

                // Create Base 64 encoded CER (public key only)
                File.WriteAllText("Certificate.cer",
                    "-----BEGIN CERTIFICATE-----\r\n"
                    + Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)
                    + "\r\n-----END CERTIFICATE-----");
            }
        }


        public static void InstallCert()
        {
            using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
            {
                var cert = new X509Certificate2("Certificate.pfx", "pwd123", X509KeyStorageFlags.PersistKeySet);
                store.Open(OpenFlags.ReadWrite);
                store.Add(cert); //where cert is an X509Certificate object
            }
        }

        private static string GetTcpHost()
        {
            return "net.tcp://localhost:5050/myservice1";
        }
    }
}

Client:

private ChannelFactory<IRemote> GetChannelFactory()
{
    var sTcp = "net.tcp://localhost:5050/myservice1"

    var myBinding = new NetTcpBinding();
    myBinding.Security.Mode = SecurityMode.TransportWithMessageCredential;
    myBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
    myBinding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Certificate;

    var endpointIdentity = EndpointIdentity.CreateDnsIdentity("MyCertificateName");
    var myEndpoint = new EndpointAddress(new Uri(sTcp), endpointIdentity);
    var factory = new ChannelFactory<IRemote>(myBinding, myEndpoint);
    factory.Credentials.UserName.UserName = User;
    factory.Credentials.UserName.Password = Password;
    factory.Credentials.ServiceCertificate.SslCertificateAuthentication =
       new X509ServiceCertificateAuthentication()
       {
           CertificateValidationMode = X509CertificateValidationMode.None,
           RevocationMode = X509RevocationMode.NoCheck
       };

    return factory;
}

User Validator:

using System;
using System.IdentityModel.Selectors;
using System.ServiceModel;

namespace Demo.Auth
{
    public class CustomUserNameValidator : UserNamePasswordValidator
    {
        // This method validates users. It allows in two users, test1 and test2
        // with passwords 1tset and 2tset respectively.
        // This code is for illustration purposes only and
        // must not be used in a production environment because it is not secure.   
        public override void Validate(string userName, string password)
        {
            if (null == userName || null == password)
            {
                throw new ArgumentNullException();
            }

            if (!"user1".Equals(userName) || !"pwd".Equals(password))
            {
                throw new FaultException("Usuário ou senha inválido");
                // When you do not want to throw an infomative fault to the client,
                // throw the following exception.
                // throw new SecurityTokenException("Unknown Username or Incorrect Password");
            }
        }
    }
}

Upvotes: 0

MichaelS
MichaelS

Reputation: 3831

Ok, got it.

I had to set the Transport CredentialType to 'Certificate' and the Message CredentialType to 'UserName'. On both sides.

This is the working code:

Server

public class Program
{
    public static void Main()
    {
        var baseAddress = new Uri("https://localhost:8080/SelfHostedUsernamePasswordService");

        using (var host = new ServiceHost(typeof(SelfHostedUsernamePasswordService), baseAddress))
        {
            var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential);
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
            binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;

            var endpoint = host.AddServiceEndpoint(typeof(ISelfHostedUsernamePasswordService), binding, baseAddress);

            var cf = new ChannelFactory<ISelfHostedUsernamePasswordService>(binding, endpoint.Address);
            cf.Credentials.ClientCertificate.SetCertificate(
                StoreLocation.LocalMachine,
                StoreName.My,
                X509FindType.FindByThumbprint,
                "0000000000000000000000000000000000000000");

            var credentialBehavior = new ServiceCredentials();
            credentialBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new UsernamePasswordValidator();
            credentialBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
            credentialBehavior.IssuedTokenAuthentication.AllowUntrustedRsaIssuers = true;
            host.Description.Behaviors.Add(credentialBehavior);

            var metadataBehavior = new ServiceMetadataBehavior();
            metadataBehavior.HttpsGetEnabled = true;
            metadataBehavior.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
            host.Description.Behaviors.Add(metadataBehavior);

            host.Open();

            Console.WriteLine("The service is ready at {0}", baseAddress);
            Console.WriteLine("Press <Enter> to stop the service.");
            Console.ReadLine();

            host.Close();
        }
    }
}

public class UsernamePasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (!string.Equals(userName, "admin", StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(password, "password", StringComparison.Ordinal))
        {
            Console.WriteLine("Validation failed.");
            throw new SecurityTokenException("Validation failed.");
        }
        Console.WriteLine("Validation successful.");
    }
}

Client

class Program
{
    static void Main()
    {
        var remoteAddress = new EndpointAddress(new Uri("https://localhost:8080/SelfHostedUsernamePasswordService"));

        var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential);
        binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;
        binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;

        using (var client = new SelfHostedUsernamePasswordServiceClient(binding, remoteAddress))
        {
            client.ClientCredentials.UserName.UserName = "admin";
            client.ClientCredentials.UserName.Password = "password";

            var result = client.GetData(12345);
            Console.WriteLine("Got result from service: {0}", result);
            Console.ReadLine();

            client.Close();
        }
    }
}

Upvotes: 5

Related Questions