Integracja Windows Identity Foundation z WCF

Autor: Piotr Zieliński

Wymagane oprogramowanie:

Wprowadzenie

Uwierzytelnienie w usługach WCF może się odbywać za pomocą Windows Identity Foundation. W tym celu wykorzystywane jest takie samo API jak przy integracji WIF z ASP .NET. Omawiany w artykule przypadek to pojedyncza usługa sieciowa wykonująca walidację użytkownika. Tokeny weryfikowane są przez TokenHandler. Z kolei reguły autoryzacji przetwarza ClaimsAuthorizationManager.

Rysunek 1. Omawiany sposób uwierzytelnienia użytkownika przez pojedynczą usługę sieciową.

Integracja WIF z WCF

  1. Otwieramy Visual Studio w trybie administratora i tworzymy usługę WCF Service Application.

  2. Dodajemy referencje do biblioteki Microsoft.IdentityModel:

    Rysunek 2. Biblioteka Microsoft.IdentityModel musi zostać dołączona do projektu.

  1. Kolejnym zadaniem jest stworzenie TokenHandlera weryfikującego poprawność wysyłanych tokenów. Jak już wspomniano, zostaną wykorzystane tokeny bazujące na nazwie i haśle użytkownika. W tym celu należy stworzyć klasę dziedziczącą po UserNameSecurityTokenHandler:
using System;
using System.IdentityModel.Tokens;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Protocols.WSIdentity;
using Microsoft.IdentityModel.Tokens;

namespace WcfService
{
    class CustomUserNameSecurityTokenHandler : UserNameSecurityTokenHandler
    {
        public override bool CanValidateToken
        {
            get
            {
                return true;
            }
        }

        public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
        {
            UserNameSecurityToken usernameToken = token as UserNameSecurityToken;
            if (usernameToken == null)
            {
                throw new ArgumentException("usernameToken", "The security token is not a valid username security token.");
            }

            string username = usernameToken.UserName;
            string password = usernameToken.Password;

            if ("Piotr" == username && "haslo" == password) 
            {
                IClaimsIdentity identity = new ClaimsIdentity();
                identity.Claims.Add(new Claim(WSIdentityConstants.ClaimTypes.Name, username));

                return new ClaimsIdentityCollection(new IClaimsIdentity[] { identity });
            }

            throw new InvalidOperationException("Nieprawidłowe hasło.");
        }
    }

}

Metoda ValidateToken służy do weryfikacji tokena. Najpierw sprawdza, czy dostarczony token jest typu UserNameSecurityToken, a następnie weryfikuje nazwę użytkownika i hasło.

  1. Kolejnym krokiem jest stworzenie klasy przyznającej prawa do konkretnych zasobów. W tym celu należy zaimplementować klasę dziedziczącą po ClaimsAuthorizationManager. Windows Identification SDK zawiera trzy klasy implementujące podstawowy mechanizm autoryzacji. Na podstawie pliku konfiguracyjnego przyznają one dostęp do wskazanych zasobów lub zabraniają go. Kod klas (MyClaimsAuthorizationManager, PolicyReader, ResourceAction) można znaleźć w SDK lub w kodzie dołączonym do artykułu.
  2. Zaimplementowany ClaimsAuthorizationManager trzeba podpiąć w pliku konfiguracyjnym. Zanim jednak tego dokonamy, musimy zdefiniować sekcję dla microsoft.identityModel:
<configSections>
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </ configSections>

Po zadeklarowaniu sekcji można przejść do zdefiniowania zaimplementowanego TokenHandlera:

<microsoft.identityModel>
        <service>
            <securityTokenHandlers>
                <remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add type="WcfService.CustomUserNameSecurityTokenHandler"/>
            </securityTokenHandlers>
        </service>
    </microsoft.identityModel>
  1. W tej samej sekcji należy podczepić również ClaimsAuthorizationManager oraz reguły przyznawania praw:
<microsoft.identityModel>
        <service>
            <securityTokenHandlers>
                <remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
                <add type="CustomUserNameSecurityTokenHandler, App_Code"/>
            </securityTokenHandlers>
            
            <claimsAuthorizationManager type="WcfService.CustomAuthorizationManager">
                <policy resource="http://localhost:2873/Service.svc" action="http://tempuri.org/IService/GetThreeDaysForecast">
                    <or>
                        <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" claimValue="Piotr"/>                        
                    </or>
                </policy>
                <policy resource="http://localhost:2873/Service.svc" action="http://tempuri.org/IService/GetTenDaysForecast">
                    <claim claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" claimValue="paul"/>
                </policy>
            </claimsAuthorizationManager>

        </service>

Wiązanie (binding) definiuje sposób komunikacji między klientem a usługą. Określa m.in. wykorzystywany protokół komunikacji, format wiadomości i sposób zabezpieczania pakietów. Ze względu na wykorzystywanie nazwy oraz hasła w celu uwierzytelnienia użytkownika, definiujemy wiązanie w następujący sposób:

<system.serviceModel>

        <bindings>
            <wsHttpBinding>
                <binding name="UsernameBinding">
                    <security mode="TransportWithMessageCredential">
                        <message clientCredentialType="UserName"/>
                    </security>
                </binding>
</wsHttpBinding>
        </bindings>   
</system.serviceModel>
  1. Następnie trzeba zdefiniować konkretny punkt dostępowy (endpoint) wykorzystujący stworzone w poprzednim kroku wiązanie:
<system.serviceModel>
        <services>
            <service name="WcfService.Service1"
                     behaviorConfiguration="WcfService.ServiceBehavior">
                <!-- Service Endpoints -->
                <endpoint address="" binding="wsHttpBinding"
                          bindingConfiguration="UsernameBinding"
                          contract="WcfService.IService1"/>
                </service>
        </services>
  1. Dodajemy rozszerzenie ConfigureServiceHostBehaviorExtensionElement, które jest niezbędne do integracji WCF z WIF:
<system.serviceModel>

        <extensions>
            <behaviorExtensions>              
                <add name="federatedServiceHostConfiguration" type="Microsoft.IdentityModel.Configuration.ConfigureServiceHostBehaviorExtensionElement, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
            </behaviorExtensions>
        </extensions>
</system.serviceModel>
  1. Po zadeklarowaniu ConfigureServiceHostBehaviorExtensionElement można rozszerzenie przyporządkować do konkretnego zachowania (behaviour):
<behaviors>
            <serviceBehaviors>
<behavior name="WcfService.ServiceBehavior">
                    <federatedServiceHostConfiguration/>
                    <!-- 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>

Hosting usługi w IIS

Ze względu na wykorzystanie protokołu https, usługa nawet do celów testowych jest umieszczana w IIS. Hosting usługi w IIS jest bardzo prosty i wszystkie operacje wykonuje się za pomocą interfejsu graficznego:

  1. Wchodzimy w panel sterowania, wybieramy narzędzia administracyjne, a następnie klikamy na Internet Information Services Manager.

    Rysunek 3. Internet Information Services Manager.

  2. Usługa sieciowa wykorzystuje protokół https, należy zatem dodać certyfikat. W tym celu klikamy na ServerCertificates.

    Rysunek 4. Certyfikaty serwera.

  3. Z menu kontekstowego wybieramy Import i plik www.fabrikam.com.pfx znajdujący się w folderze o nazwie Pliki, dołączonym do artykułu.

  4. Dodajemy nową stronę: klikamy prawym przyciskiem myszy na węźle Sites (panel Connections) i wybieramy Add Web Site.

  5. W nowo otwartym oknie podajemy dowolną nazwę strony (np. WcfService). Należy również określić prawidłową ścieżkę do usługi sieciowej oraz wybrać protokół https i certyfikat serwera.

    Rysunek 5. Prawidłowa konfiguracja usługi WCF.

  6. Usługa wykorzystuje dwa protokoły. HTTPS zdefiniowany w poprzednim kroku używany jest do bezpiecznej komunikacji, z kolei protokół HTTP − do wyeksponowania metadanych. Aby dodać obsługę HTTP, trzeba z menu kontekstowego wybrać Edit Binding, a następnie dodać odpowiednie wiązanie. W przeciwnym wypadku podczas próby połączenia się z usługą pojawi się następujący błąd:

    The HttpGetEnabled property of ServiceMetadataBehavior is set to true and the HttpGetUrl property is a relative address, but there is no http base address.  Either supply an http base address or set HttpGetUrl to an absolute address.

    Rysunek 6. Konfiguracja usługi wymaga zarówno obsługi http, jak i https.

  7. Certyfikat fabrikam.com został wygenerowany ręcznie i nie jest oczywiście wydany przez tzw. urząd certyfikacji (certificate authority). Wszelkie próby jego weryfikacji zakończą się niepowodzeniem. Z tego względu musimy zainstalować drugi certyfikat − www.adatum.com. Należy go dodać do grupy Trusted Root Certification Authorities. Dzięki temu system będzie ufał certyfikatom wydawanym przez fikcyjny urząd certyfikacji adatum. Rzecz jasna w środowisku produkcyjnym trzeba korzystać z prawdziwych certyfikatów, wydawanych przez autoryzowane urzędy. Instalacja certyfikatu CA jest również prosta. Wystarczy wejść w jego ustawienia, np. za pomocą przeglądarki IE: Tools->Internet Options->Content->Certificates->Trusted Root Certification Authorities, i kliknąć na przycisk Import. Kreator poprosi nas o ścieżkę do certyfikatu. Po podaniu ścieżki do adatum.sst (patrz: dołączony kod źródłowy) certyfikat zostanie zaimportowany.

    Rysunek 7. Import certyfikatu CA.

  8. Gdybyśmy teraz uruchomili usługę w przeglądarce, pojawiłby się komunikat o błędzie, ponieważ certyfikat został nadany stronie www.fabrikam.com, a nie www.localhost.com. Dlatego należy wyedytować specjalny plik hosts znajdujący się w C:\Windows\System32\drivers\etc. Zawiera on mapowania różnych adresów. Wystarczy zatem dodać w pliku następujący wpis, który zmapuje localhost na www.fabrikam.com:

    127.0.0.1 www.fabrikam.com

  9. Po wpisaniu https://www.fabrikam.com/Service1.svc w przeglądarce certyfikat powinien zostać już pozytywnie zweryfikowany.

    Rysunek 8. Pasek adresu po pozytywnej weryfikacji powinien być zielony.

Aplikacja kliencka

  1. Tworzymy nową aplikację WPF.

  2. Dodajemy referencję do usługi. W tym celu prawym przyciskiem myszy klikamy na nazwie projektu (Solution Explorer) i wybieramy Add Service Reference.

    Rysunek 9. Dodawanie referencji do usługi sieciowej.

  3. Otwieramy plik konfiguracyjny App.Config. Znajdujemy element endpoint, który powinien być w węźle system.serviceModel/client. Upewniamy się, że adres usługi został prawidłowo zdefiniowany:

<client>
            <endpoint address="https://www.fabrikam.com/Service1.svc" binding="wsHttpBinding"
                bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1"
                name="WSHttpBinding_IService1" />
     </client>
  1. Spróbujmy wywołać metodę GetDataForAllUsers. Według zdefiniowanej polityki bezpieczeństwa zarówno użytkownik UserA, jak i UserB po poprawnym uwierzytelnieniu mogą wywoływać tę metodę. Najpierw wywołamy metodę, przekazując błędne hasło:
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            
            client.ClientCredentials.UserName.UserName = "UserB";
            client.ClientCredentials.UserName.Password = "InvalidPassword";
            string strNumber = client.GetDataForAllUsers(5);
            Debug.Assert(strNumber == "5");

Próba się nie powiedzie, pojawi się komunikat: An error occurred when verifying security for the message.

  1. Teraz podamy prawidłowe hasło, aby przekonać się, że dostęp faktycznie zostanie nadany:
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            
            client.ClientCredentials.UserName.UserName = "UserB";
            client.ClientCredentials.UserName.Password = "UserBPassword";
            string strNumber = client.GetDataForAllUsers(5);
            Debug.Assert(strNumber == "5");
  1. Spróbujmy się przekonać, co się stanie w przypadku prawidłowego uwierzytelnia, ale błędnej autoryzacji. Podamy poprawne dane dla UserB (poprawne uwierzytelnienie), ale będziemy chcieli wywołać metodę GetDataForUserA, do której dostęp został nadany wyłącznie użytkownikowi UserA (błędna autoryzacja):
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            
            client.ClientCredentials.UserName.UserName = "UserB";
            client.ClientCredentials.UserName.Password = "UserBPassword";
            string strNumber = client.GetDataForUserA(5);
            Debug.Assert(strNumber == "5");

Po chwili pojawi się wyjątek SecurityAccessDeniedException, zawierający treść: „Access is denied”.

  1. Pozostało tylko przekonać się, że dostęp do metody GetDataForUserA zostanie poprawnie nadany użytkownikowi UserA:
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            
            client.ClientCredentials.UserName.UserName = "UserA";
            client.ClientCredentials.UserName.Password = "UserAPassword";
            string strNumber = client.GetDataForUserA(5);
            Debug.Assert(strNumber == "5");

Materiały dodatkowe: