Reduce repeated nested identity in WCF config

394 Views Asked by At

My web.config for my WCF app has a series of endpoints defined like so:

<system.serviceModel>
    <services>
        <service behaviorConfiguration="whatever" name="MyService">
            <endpoint name="Endpoint1" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract1">
                <identity>
                    <certificateReference findValue="cert name storeName="TrustedPeople" storeLocation="LocalMachine" x509FindType="FindBySubjectName" />
                </identity>
            </endpoint>
            <endpoint name="Endpoint2" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract2">
                <identity>
                    <certificateReference findValue="cert name storeName="TrustedPeople" storeLocation="LocalMachine" x509FindType="FindBySubjectName" />
                </identity>
            </endpoint>
            <endpoint name="Endpoint3" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract3">
                <identity>
                    <certificateReference findValue="cert name storeName="TrustedPeople" storeLocation="LocalMachine" x509FindType="FindBySubjectName" />
                </identity>
            </endpoint>
            <endpoint name="Endpoint4" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract4">
                <identity>
                    <certificateReference findValue="cert name storeName="TrustedPeople" storeLocation="LocalMachine" x509FindType="FindBySubjectName" />
                </identity>
            </endpoint>

What I would like to do is

<system.serviceModel>
    <services>
        <service behaviorConfiguration="whatever" name="MyService">
            <endpoint name="Endpoint1" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract1" />
            <endpoint name="Endpoint2" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract2" />
            <endpoint name="Endpoint3" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract3" />
            <endpoint name="Endpoint4" address="" binding="customBinding" bindingConfiguration="HttpIssuedTokenBinding" contract="My.App.Contract4" />

with a default identity definition specified once in another place (even just a top level in the system.serviceModel element).

Basically I want to DRY, because the config is consistent all the way throughout. What I need help from SO is where does one find the "default identity for all endpoints" configuration element. MSDN isn't giving a lot of help, and I'm not sure where to reflect the .NET libs to see how this is interpreted when web.configs are read in at app startup.

1

There are 1 best solutions below

3
On

Summary

Use Standard Endpoints to create a custom endpoint with the appropriate identity information, which can be configured from the configuration file.


Detail

Thank you for the question!. The uniform configuration of WCF services to reduce the configuration overhead is something I've been meaning to look into, and your question gave me just the excuse to do it.

I tackled this using Standard Endpoints, which have been around since .NET 4. The bulk of the work is done by inheriting from StandardEndpointElement:

namespace WcfEx
{
    public class X509EndpointElement : StandardEndpointElement
    {
        private static string _findValueKey = "findValue";
        private static string _storeNameKey = "storeName";
        private static string _storeLocationKey = "storeLocation";
        private static string _x509FindTypeKey = "x509SearchType";

        public virtual string FindValue
        {
            get { return base[_findValueKey] as string; }
            set { base[_findValueKey] = value; }
        }

        public virtual StoreName StoreName
        {
            get { return this[_storeNameKey] is StoreName ? (StoreName) this[_storeNameKey] : (StoreName) 0; }
            set { this[_storeNameKey] = value; }
        }

        public virtual StoreLocation StoreLocation
        {
            get
            {
                return this[_storeLocationKey] is StoreLocation
                           ? (StoreLocation) this[_storeLocationKey]
                           : (StoreLocation) 0;
            }
            set { this[_storeLocationKey] = value; }
        }

        public virtual X509FindType X509FindType
        {
            get { return this[_x509FindTypeKey] is X509FindType ? (X509FindType) this[_x509FindTypeKey] : (X509FindType) 0; }
            set { this[_x509FindTypeKey] = value; }
        }

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                ConfigurationPropertyCollection properties = base.Properties;
                properties.Add(new ConfigurationProperty(_findValueKey, typeof (string), null,
                                                         ConfigurationPropertyOptions.None));
                properties.Add(new ConfigurationProperty(_storeNameKey, typeof (StoreName), null,
                                                         ConfigurationPropertyOptions.None));
                properties.Add(new ConfigurationProperty(_storeLocationKey, typeof (StoreLocation), null,
                                                         ConfigurationPropertyOptions.None));
                properties.Add(new ConfigurationProperty(_x509FindTypeKey, typeof (X509FindType), null,
                                                         ConfigurationPropertyOptions.None));
                return properties;
            }
        }

        protected override Type EndpointType
        {
            get { return typeof (ServiceEndpoint); }
        }

        protected override ServiceEndpoint CreateServiceEndpoint(ContractDescription contract)
        {
            return new ServiceEndpoint(contract);
        }

        protected override void OnApplyConfiguration(ServiceEndpoint endpoint,
                                                     ServiceEndpointElement serviceEndpointElement)
        {
            endpoint.Address = new EndpointAddress(endpoint.Address.Uri,
                                                   EndpointIdentity.CreateX509CertificateIdentity(
                                                       GetCertificateFromStore()));
        }

        protected override void OnApplyConfiguration(ServiceEndpoint endpoint,
                                                     ChannelEndpointElement channelEndpointElement)
        {
            endpoint.Address = new EndpointAddress(endpoint.Address.Uri,
                                                   EndpointIdentity.CreateX509CertificateIdentity(
                                                       GetCertificateFromStore()));
        }

        private X509Certificate2 GetCertificateFromStore()
        {
            var certificateStore = new X509Store(StoreName, StoreLocation);
            certificateStore.Open(OpenFlags.ReadOnly);
            var matchingCertificates = certificateStore.Certificates.Find(X509FindType, FindValue, false);

            X509Certificate2 matchingCertificate = null;
            if (matchingCertificates.Count > 0)
                matchingCertificate = matchingCertificates[0];
            else throw new InvalidOperationException("Could not find specified certificate");

            certificateStore.Close();
            return matchingCertificate;
        }

        protected override void OnInitializeAndValidate(ChannelEndpointElement channelEndpointElement)
        {

        }

        protected override void OnInitializeAndValidate(ServiceEndpointElement serviceEndpointElement)
        {

        }
    }

}

A quick summary of the above code:

  • This class can be made visible in the .config file - so it's properties can be set through configuration;
  • There are four properties that specify the parameters for selecting the X509 certificate;
  • At some point in the lifetime of this class, the endpoint address is set with the identity specified by a certificate satisfying the search criteria.

You need a collection class in which to hold elements of the above class:

namespace WcfEx
{
    public class X509EndpointCollectionElement : StandardEndpointCollectionElement<ServiceEndpoint, X509EndpointElement>
    {

    }
}

The system.serviceModel section of the .config file looks like this:

<system.serviceModel>
<extensions>
  <endpointExtensions>
    <add name="x509Endpoint" type="WcfEx.X509EndpointCollectionElement, WcfEx, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </endpointExtensions>
</extensions>
<standardEndpoints>
  <x509Endpoint> 
    <standardEndpoint storeLocation="LocalMachine" storeName="TrustedPeople" findValue="cert name" x509SearchType="FindBySubjectName"/>
  </x509Endpoint>
</standardEndpoints> 
<services>
  <service name="WcfHost.Service1">
    <endpoint address="" binding="wsHttpBinding" contract="WcfHost.IService1" kind="x509Endpoint" >
    </endpoint>
  </service>
</services> 
<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceMetadata httpGetEnabled="true"/>
      <serviceDebug includeExceptionDetailInFaults="false"/>
    </behavior>
  </serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Things to note:

  • Be very careful about the value of the type attribute here - it needs to be exactly the same as typeof (X509EndpointElement).AssemblyQualifiedName.
  • Once registered, we can add the relevant configuration to the standardEndpoints element.
  • We "tag" an endpoint as having the customisation by using the kind attribute.