Custom authentication w WCF

W celu zaimplementowania customowego uwierzytelniania użytkownika chcącego wykonać operacje w serwisie WCF, można skorzystać z Username Password Authentication.

Rozwiązanie to wymaga dodatkowo posiadania/wygenerowania certyfikatu X509.

Korzystamy z bindingu wsHttpBinding z włączanym zabezpieczeniem transmisji na poziome komunikatów.
Dodatkowo właściwość clientCredentialType ustawiamy na UserName.

      <wsHttpBinding>
        <binding name="WSHttpBindingCalculatorService">
          <security mode="Message">
            <message clientCredentialType="UserName" />
          </security>
        </binding>
      </wsHttpBinding>

Następnie w serwice behaviors definiujemy sekcje serviceCredentials, gdzie ustawiamy userNameAuthentication i określamy, że chcemy sami dostarczyć metodę do walidacji loginu i hasła.
W metodzie tej zaimplementujemy logikę, w której sprawdzimy czy podany login i hasło zgadzają się.
Ustawiamy tutaj również serviceCertificate.

      <serviceBehaviors>        
        <behavior name="CalculatorBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom"
              customUserNamePasswordValidatorType="CalculatorServiceLibrary.CredentialsValidator, CalculatorServiceLibrary" />
            <serviceCertificate findValue="MyComp" storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>

Metoda do walidacji sprowadza się do zaimplementowanie metody Validate z abstrakcyjnej klasy UserNamePasswordValidator.

    public class CredentialsValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))

                throw new SecurityTokenException("Username and password required");

            if (!IsCredentialValid(userName, password))
            {
                throw new FaultException("Wrong username or password ");
            }
        }

        private bool IsCredentialValid(string userName, string password)
        {
             //Sprawdzenia czy podany userName i password zgadzają się
        }
    }

Samo przekazanie username i password do serwisu sprowadza się do ustawienia właściwości ClientCredentials.UserName w obiekcie proxy.
Użytkownik podaje je w momencie kiedy loguje się np. do aplikacji webowej.

  proxy.ClientCredentials.UserName.UserName = "login";
  proxy.ClientCredentials.UserName.Password = "password";
Advertisements

WCF, net.tcp i svcutil

Chcąc wystawić WCF po protokole innym niż http, np.: net.tcp, i mieć możliwość wygenerowania Proxy dla naszego serwisu, musimy wykonać aktywację TCP. Czynności z tym związane opisane są na stronach MSDN.
Generalnie sprawa sprowadza się do dodania dodatkowego endpointu do naszego serwisu z bindingiem mexTcpBinding i kontraktem IMetadataExchange.

<endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />

Po tych czynnościach jesteśmy w stanie wygenerować Proxy narzędziem svcutil.exe, np.:

svcutil net.tcp://localhost/MyService/Service1.svc/mex

Globalne przchwytywanie wyjatków w WCF

using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Collections.ObjectModel;
using System;

namespace Framework.Services
{
    public class ServiceErrorHandlerBehaviorAttribute : Attribute, IServiceBehavior
    {
        private readonly IErrorHandler errorHandler;

        public ServiceErrorHandlerBehaviorAttribute()
        {
            errorHandler = new ServiceErrorHandler();
        }

        public ServiceErrorHandlerBehaviorAttribute(IErrorHandler errorHandler)
        {
            this.errorHandler = errorHandler;
        }

        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var channelDispatchers = serviceHostBase.ChannelDispatchers;
            var length = channelDispatchers.Count;
            for (var i = 0; i < length; ++i)
            {
                var cd = channelDispatchers[i] as ChannelDispatcher;
                if (cd != null)
                {
                    if (!cd.ErrorHandlers.Contains(errorHandler))
                    {
                        cd.ErrorHandlers.Add(errorHandler);
                    }
                }
            }
        }

        public void AddBindingParameters(
            ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection endpoints, BindingParameterCollection bindingParameters)
        {
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }
    }
}

using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

namespace Framework.Services
{
    public class ServiceErrorHandler : IErrorHandler 
    {        
        private static readonly bool SaveErrorInLog; 
        
        static ServiceErrorHandler()
        {
	  //get value from web.config
            SaveErrorInLog = true;
        } 
        
        public bool HandleError(Exception error)
        {
            if (SaveErrorInLog)
	      HandleErrorLogic(error);
            
            return !(error is FaultException);
        }

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            if (SaveErrorInLog)
	      HandleErrorLogic(error);
            
            if (!(error is FaultException))
            {
                var messageFault = MessageFault.CreateFault(
                    new FaultCode("MessageFault.Code"), new FaultReason(error.Message), error, new NetDataContractSerializer());
                fault = Message.CreateMessage(version, messageFault, null);
            }
        }
        
        private void HandleErrorLogic(Exception error)
        {
            try
            {
                //zapisz błąd w logu
            }
            catch
            {
            }
        }         
    }
}

    [ServiceErrorHandlerBehavior]
    public class MyService : IMyService
    {
          public void DoSomething()
          {
          }
    }

IoC w WCF z wykorzystaniem Microsoft Unity

UnityInstanceProvider.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using Microsoft.Practices.Unity;
using System.ServiceModel.Channels;
using System.ServiceModel;

namespace Toolkit.Unity
{
    internal class UnityInstanceProvider : IInstanceProvider
    {
        private readonly IUnityContainer container;
        private readonly Type contractType;

        public UnityInstanceProvider(IUnityContainer container, Type contractType)
        {
            this.container = container;
            this.contractType = contractType;

        }

        public object GetInstance(InstanceContext instanceContext, Message message)
        {
            return container.Resolve(contractType);
        }

        public object GetInstance(System.ServiceModel.InstanceContext instanceContext)
        {
            return GetInstance(instanceContext, null);
        }

        public void ReleaseInstance(System.ServiceModel.InstanceContext instanceContext, object instance)
        {
            container.Teardown(instance);
        }
    }
}

UnityServiceBehavior.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel;
using Microsoft.Practices.Unity;
using System.ServiceModel.Dispatcher;

namespace Toolkit.Unity
{
    public class UnityServiceBehavior : IServiceBehavior
    {
        private readonly IUnityContainer container;

        public UnityServiceBehavior(IUnityContainer container)
        {
            this.container = container;
        }

        public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {

        }

        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {

            foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
            {
                foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
                {                    
                    endpointDispatcher.DispatchRuntime.InstanceProvider = new UnityInstanceProvider(container, serviceDescription.ServiceType);                    
                }
            }
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {

        }
    }
}

UnityServiceHost.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Microsoft.Practices.Unity;

namespace Toolkit.Unity
{
    public class UnityServiceHost : ServiceHost
    {
        private IUnityContainer unityContainer;

        public UnityServiceHost(IUnityContainer unityContainer, Type serviceType, params Uri[] baseAddresses)
            : base(serviceType, baseAddresses)
        {
            this.unityContainer = unityContainer;
        }

        protected override void OnOpening()
        {
            base.OnOpening();

            if (this.Description.Behaviors.Find() == null)
            {
                this.Description.Behaviors.Add(new UnityServiceBehavior(this.unityContainer));
            }
        }
    }
}

UnityServiceHostFactory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.ServiceModel;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System.ServiceModel.Activation;

namespace Toolkit.Unity
{
    public class UnityServiceHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            var unityContainer = new UnityContainer();
            var configurationSection = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
            configurationSection.Configure(unityContainer);

            UnityServiceHost serviceHost = new UnityServiceHost(unityContainer, serviceType, baseAddresses);

            return serviceHost;
        }
    }
}

Definicja Factory dla serwisu WCF

<%@ ServiceHost Language="C#" Debug="true"
    Service="WCFUnityInjection.BookService"
    Factory ="Toolkit.Unity.UnityServiceHostFactory"
    CodeBehind="BookService.svc.cs" %>

web.config dla serwisu WCF

<?xml version="1.0"?>
<configuration>
    <configSections>
        <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
    </configSections>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
    </system.web>
    <system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior>
                    <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
                    <serviceMetadata httpGetEnabled="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="false"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
    </system.serviceModel>
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
    </system.webServer>
    <unity>
        <containers>
            <container>                
                <types>
                    <type type="WCFUnityInjection.IInnerBookService, WCFUnityInjection" mapTo="WCFUnityInjection.InnerBookService, WCFUnityInjection"/>
                </types>                    
            </container>
        </containers>
    </unity>
</configuration>

Implementacja intefrejsu IClientMessageInspector

Implementacja intefrejsu IClientMessageInspector umożliwiającego odczytanie i modyfikację komunikatu soap otrzymanego z serwisu. Dodatkowo należy zaimplementować interfejs IEndpointBehavior, za pomocą którego definiujemy własny behavior oraz napisać swój BehaviorExtensionElement co daje nam możliwość wykorzystania behaviora bezpośrednio w pliku konfiguracyjnym.

 namespace ConsoleMyApp
 {
     public class MyBehavior : IEndpointBehavior
     {
         #region IEndpointBehavior Members

         public void AddBindingParameters(
             ServiceEndpoint endpoint,
             BindingParameterCollection bindingParameters)
         {
         }

         public void ApplyClientBehavior(
             ServiceEndpoint endpoint,
             ClientRuntime clientRuntime)
         {
             clientRuntime.MessageInspectors.Add(new MyMessageInspector());
         }

         public void ApplyDispatchBehavior(
             ServiceEndpoint endpoint,
             EndpointDispatcher endpointDispatcher)
         {
         }

         public void Validate(
             ServiceEndpoint endpoint)
         {
         }

         #endregion
     }

     public class MyMessageInspector : IClientMessageInspector
     {
         #region IClientMessageInspector Members

         public void AfterReceiveReply(
             ref Message reply,
             object correlationState)
         {
             Console.WriteLine(
             "Received the following reply: '{0}'", reply.ToString());           
         }

         public object BeforeSendRequest(
             ref Message request,
             IClientChannel channel)
         {
             Console.WriteLine(
             "Sending the following request: '{0}'", request.ToString());
             return null;
         }

         #endregion
     }

     public class CustomBehaviorExtensionElement : BehaviorExtensionElement
     {
         protected override object CreateBehavior()
         {
             return new MyBehavior();
         }

         public override Type BehaviorType
         {
             get
             {
                 return typeof(MyBehavior);
             }
         }
     }
 }

I przykładowy app.config

<?xml version="1.0" encoding="utf-8" ?>
 <configuration>
   <system.diagnostics>
     <sharedListeners>
       <add name="System.Net" type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\logs\System.Net.trace.txt" traceOutputOptions="DateTime" />
     </sharedListeners>
     <sources>
       <source name="System.ServiceModel.MessageLogging">
         <listeners>
           <add name="messages"
           type="System.Diagnostics.XmlWriterTraceListener"
           initializeData="c:\logs\messagesCC.svclog" />
         </listeners>
       </source>
       <source name="System.ServiceModel" switchValue="All" propagateActivity="true">
         <listeners>
           <add name="messages"
           type="System.Diagnostics.XmlWriterTraceListener"
           initializeData="c:\logs\diagnosticsCC.svclog" />
         </listeners>
       </source>
       <source name="System.Net" switchValue="All">
         <listeners>
           <add name="System.Net"/>
         </listeners>
       </source>
       <source name="System.Net.HttpListener" switchValue="All">
         <listeners>
           <add name="System.Net"/>
         </listeners>
       </source>
       <source name="System.Net.Sockets" switchValue="All">
         <listeners>
           <add name="System.Net"/>
         </listeners>
       </source>
       <source name="System.Net.Cache" switchValue="All">
         <listeners>
           <add name="System.Net"/>
         </listeners>
       </source>
     </sources>
   </system.diagnostics>
   <system.serviceModel>
     <bindings>
       <basicHttpBinding>
         <binding name="PhBinding" closeTimeout="00:01:00" openTimeout="00:01:00"
           receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false"
           bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
           maxBufferSize="524288" maxBufferPoolSize="524288" maxReceivedMessageSize="524288"
           messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
           useDefaultWebProxy="true">
           <readerQuotas maxDepth="32" maxStringContentLength="5242880"
             maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
           <security mode="None">
             <transport clientCredentialType="None" proxyCredentialType="None"
               realm="" />
             <message clientCredentialType="UserName" algorithmSuite="Default" />
           </security>
         </binding>        
       </basicHttpBinding>
       <wsHttpBinding>
         <binding name="customWsHttpBinding">
           <security mode="None" />
         </binding>
       </wsHttpBinding>
     </bindings>
     <client>
       <endpoint address="ADDRESS_SERVISU" behaviorConfiguration="Behavior"
         binding="basicHttpBinding" bindingConfiguration="PhBinding"
         contract="MyService.PhPortType" name="PhPort" />
     </client>
     <extensions>
       <behaviorExtensions>
         <add name="BehaviorElement"
           type="ConsoleMyApp.CustomBehaviorExtensionElement, ConsoleMyApp"/>
       </behaviorExtensions>
     </extensions>
     <behaviors>
       <endpointBehaviors>
         <behavior name="Behavior">
           <BehaviorElement />
         </behavior>
       </endpointBehaviors>
     </behaviors>
     <diagnostics>
       <messageLogging
            logEntireMessage="true"
            logMalformedMessages="false"
            logMessagesAtServiceLevel="true"
            logMessagesAtTransportLevel="false"
            maxMessagesToLog="3000"
            maxSizeOfMessageToLog="2000"/>
     </diagnostics>
   </system.serviceModel>
 </configuration>

Could not load type ‘System.ServiceModel.Activation.HttpModule’

Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.
Description: An unhandled exception occurred during the execution of the current Web request. Review the stack trace for more information about the error and where it originated in the code.
 Exception Details: System.TypeLoadException: Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

Taki błąd pojawia się jeśli instalujemy IIS i .NET Framework w odwróconej kolejności, a mianowicie wtedy, kiedy IIS jest instalowany po zainstalowaniu .NET Framework, albo jeśli WCF Http Activation moduł jest instalowany po zainstalowaniu IIS i .NET Framework.
Problem ten można rozwiązać za pomocą ASP.NET IIS Registration Tool (aspnet_regiis.exe) .
Należy uruchomić to polecenie z parametrami -iru:

aspnet_regiis.exe -iru

Dodatkowo przydatne może okazać się również poniższe polecenie:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319>ServiceModelReg.exe -r