Często WCF, mimo swoich możliwości w zakresie “interoperability”, wcale nie musi być kompatybilny z komponentami zewnętrznymi. Nasz serwer, nasz klient, a WCF między nimi. I… tu zwykle zaczynają się problemy… (jak to pisał nie-ś.p. † Kurt Vonnegut, gówno wpada w szprychy:) ).
WCF jest tak rozbudowaną i skomplikowaną technologią, że odpowiednie dobranie zawartych w niej klocków do stworzenia budowli, której potrzebujemy, jest niekiedy żmudnym, trudnym i bardzo czasochłonnym zajęciem. Niby na MSDN jest masa materiału, niby mamy do dyspozycji wiele książek (w tym wyś-mie-ni-ta “Programming WCF Services” by Lowy Juval, polecam!)… Niby to jest takie “proste” że aż głupio przyznać się, jeśli spędza się nad czymś dużo czasu – bo przecież wystarczy kilka zmian w konfiguracji i mamy co trzeba.
Ale ja wyjdę z założenia, że prosty to jest kręgosłup programisty (zanim stanie się programistą) a nie WCF. I przyznam się, że kiedyś prawie trzy noce spędziłem nad realizacją takiego PODSTAWOWEGO scenariusza:
- serwer udostępniający funkcjonalność poprzez usługi WCF i klient korzystający z tych usług
- komunikacja kanałem net.tcp z binarną serializacją (klient i serwer są moje, nie potrzebuję protokołu HTTP ani serializacji SOAP)
- uwierzytelnianie poprzez login i hasło
Banalne…? Może i tak, ale próżno szukać omówienia takiego przykładu w, bardzo skądinąd interesującym, poradniku stworzonym przez zespół Patterns And Practices: Scenarios and Implementation Guidance for WCF. Znajdziemy tam zabezpieczenie username/password przez HTTP zintegrowane z ASP.NET, znajdziemy bezpieczną serializację binarną w środowisku intranetowym, znajdziemy wiele innych ciekawych zastosowań. Ale najprostszego, wydawałoby się, username/password po TCP bez żadnych dodatkowych mechanizmów – nie.
Ogólnie…
Przykładowe rozwiązanie (dostępne do ściągnięcia na stronie Samples) składa się z pięciu projektów. Jest to dość sensowne, dość logiczne i dość standardowe rozdzielenie odpowiedzialności pomiędzy dllki. A że jeden obraz wart tysiąca słów…:
Common
Projekty tu zawarte są współdzielone przez klientów i serwer. Zawierają tak banalne informacje jak zdefiniowane w aplikacji nazwy uprawnień czy klasy Identity i Principal. Znajdziemy tam też interfejsy kontraktów usług, co w przypadku, gdy implementujemy jednocześnie i klienta i serwer jest sensowniejszym rozwiązaniem niż generowanie kopii w VS na podstawie WSDL.
Serwer
Na serwerze znajduje się… logika serwerowa:). Czyli dostęp do danych, obiekty pseudo-biznesowe, implementacje usług… Oraz, co najważniejsze, cała machineria dostosowująca WCF do naszych potrzeb. Będzie tu zatem obsługa wyjątków zrealizowana w sposób opisany kiedyś (“Obsługa wyjątków w usługach WCF“), będzie też rozszerzona wersja własnych mechanizmów uwierzytelniania (“Własne mechanizmy uwierzytelniania w WCF“). I właśnie tymi rozszerzeniami zajmiemy się w tej chwili.
Po dłubaniu w internecie i własnych eksperymentach pojawiły sie następujące obserwacje:
- jeżeli i klient i serwer to .NET, wtedy najlepiej posłużyć się binarną serializacją i komunikacją poprzez protokół TCP, bez narzutu generowanego przez XML i HTTP; wybór jest zatem prosty: NetTcpBinding
- tryb bezpieczeństwa używany przez binding musi umożliwiać uwierzytelnianie za pomocą loginu i hasła, zatem z czterech opcji dostępnych w enumie SecurityMode zostają nam Message i TransportWithMessageCredential; z tych dwóch punktów uzyskujemy binding:
1: NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.Message);
- przy definicji sposobu uwierzytelniania wybierzemy oczywiście opcję MessageCredentialType.UserName:
1: tcpBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
- sposobów weryfikacji poprawności loginu i hasła jest kilka, nas interesuje możliwość wpięcia własnej implementacji UserNamePasswordValidator…
1: host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom; 2: host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = _serverSecurity;
- …natomiast autoryzację załatwiamy implementacją IAuthorizationPolicy:
1: host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom; 2: host.Authorization.ExternalAuthorizationPolicies = new ReadOnlyCollection<IAuthorizationPolicy>(new[] { _serverSecurity });
Jeszcze raz link do posta z pokazanymi klasami: “Własne mechanizmy uwierzytelniania w WCF“.
I takie coś wydawało się sensowne, jednak w momencie otwierania hosta pojawiał się wyjątek:
“The service certificate is not provided. Specify a service certificate in ServiceCredentials.”
I jego wyeliminowanie zajęło mi dalszy kawał czasu.
Okazuje się, że włączenie SecurityMode na Message bądź TransportWithMessageCredential skutkuje wymaganiem posiadania certyfikatu w celu umożliwienia wiarygodnej identyfikacji serwera przez klienta. I z tego co sie naszukałem, NIE DA się tego wyłączyć. Natknąłem się nawet w internecie na jakąś własną implementację bindingu, która naokoło umożliwia uwierzytelnianie username/password bez dodatkowych zabezpieczeń, ale wydaje się to być rozwiązaniem bardzo na siłę.
Poszedłem więc inną drogą i wygenerowałem sobie certyfikat. Znalezienie odpowiedniej sekwencji komend wcale nie było proste, jeśli nie miało się z certyfikatami wcześniej do czynienia. Po kolei więc przedstawiam swoje kroki (może są głupie…?), których jedynym celem jest dostarczenie poprawnego certyfikatu pozwalającego na komunikację klient-serwer w kontrolowanym środowisku:
1) używamy narzędzie makecert w celu stworzenia pary publiczny/prywatny klucz; ustawiamy odpowiednie flagi pozwalające na dość frywolne obchodzenie się z kluczem prywatnym w kroku kolejnym, a w wyskakującym okienku możemy zaakceptować puste hasło:
makecert -sv WcfAuthSampleKey.pvk -n “CN=ProcentWcfSample” WcfAuthSampleKey.cer -pe -sky exchange
2) za pomocą narzędzia pvk2pfx generujemy plik PFX zawierający informacje o naszych kluczach:
pvk2pfx.exe -pvk WcfAuthSampleKey.pvk -spc WcfAuthSampleKey.cer -pfx WcfAuthSampleKey.pfx
3) tak uzyskany plik .pfx dodajemy do projektu ustawiając jego Build Action na Embedded Resource
Dzięki temu plik certyfikatu mamy wkompilowany w serwer i jest on samowystarczalny (UWAGA, takie rozwiązanie z pewnością nie jest oczywiście poprawnym rozwiązaniem kwestii certyfikatu w publicznie działającej usłudze:) )
Zaaplikowanie tak spreparowanego certyfikatu podczas uruchamiania hostów wygląda w kodzie następująco:
1: using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream( 2: "Procent.Samples.WcfAuthStarter.Server.misc.WcfAuthSampleKey.pfx") 3: ) 4: { 5: byte[] bytes = new byte[stream.Length]; 6: stream.Read(bytes, 0, bytes.Length); 7: host.Credentials.ServiceCertificate.Certificate = new X509Certificate2(bytes, string.Empty); 8: }
Jasne jest, że taka procedura skutecznie uniemożliwia wykorzystanie na serwerze plików konfiguracyjnych w celu zmiany ustawień WCFa.
Cała metoda otwierająca hosta wygląda dzięki typom generycznym całkiem ładnie:
1: private static ServiceHost OpenHost<TContract, TImplementation>(string baseUri, string address) where TImplementation : TContract 2: { 3: ServiceHost host = new ServiceHost(typeof(TImplementation), new Uri(baseUri)); 4: 5: // transfer security mode that enables username/password authentication 6: NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.Message); 7: 8: // client authentication configuration 9: tcpBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; 10: // use custom logic to verify username/password (not MembershipProvider) 11: host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom; 12: host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = _serverSecurity; 13: 14: // client authorization configuration 15: host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom; 16: // adding AuthorizationPolicy allows to set custom IPrincipal/IIdentity implementations for the Operation 17: host.Authorization.ExternalAuthorizationPolicies = new ReadOnlyCollection<IAuthorizationPolicy>(new[] { _serverSecurity }); 18: 19: // server identity verification by X.509 certificate (required by Message transfer security) 20: using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Procent.Samples.WcfAuthStarter.Server.misc.WcfAuthSampleKey.pfx")) 21: { 22: byte[] bytes = new byte[stream.Length]; 23: stream.Read(bytes, 0, bytes.Length); 24: host.Credentials.ServiceCertificate.Certificate = new X509Certificate2(bytes, string.Empty); 25: } 26: 27: host.AddServiceEndpoint(typeof(TContract), tcpBinding, address); 28: 29: // add behavior that handles exceptions: logs and converts to faults 30: host.Description.Behaviors.Add(new ErrorHandlingBehavior()); 31: 32: host.Open(); 33: 34: Console.WriteLine("Opened service {0} implemented by {1} under address {2}", typeof(TContract).Name, typeof(TImplementation).Name, baseUri + address); 35: 36: return host; 37: }
A przykładowa deklaracja metody z usługi wygląda tak:
1: public class ProductsService : IProductsService 2: { 3: [PrincipalPermission(SecurityAction.Demand, Role = AppRoles.ProductsAdmin)] 4: public void AddProduct(string name, double price) 5: {
Klient
Logika kliencka podzielona została na dwie biblioteki.
Jedna z nich (Client.Core) ma na celu dostarczenie mechanizmów dla wszystkich implementacji klienta, niezależnie od wykorzystanej technologii. W przykładzie znajduje się tam jedynie opisywane przeze mnie już kiedyś WCF Proxy (“Własna implementacja WCF Proxy“).
Druga biblioteka pisana jest pod konkretną technologię, w tym przypadku: Windows Forms. Znajdziemy tutaj między innymi kolejną warstwę komunikacji z WCF (także opisaną we wspomnianym wpisie). Mała różnica: tym razem przed wywołaniem każdej metody wypełniamy dane do uwierzytelnienia:
1: public static void Invoke(Action<TService> operation) 2: { 3: using (var proxy = new ServiceProxy<TService>()) 4: { 5: proxy.ClientCredentials.UserName.UserName = CurrentData.Credentials.Username; 6: proxy.ClientCredentials.UserName.Password = CurrentData.Credentials.Password; 7: 8: operation(proxy.GetChannel()); 9: } 10: }
Inny godny uwagi mechanizm to dynamiczne budowanie UI na podstawie uprawnień użytkownika. Swego czasu po opisaniu tego rozwiązania zostałem dość mocno na skrytykowany w komentarzach (post “Dynamiczne budowanie UI zależne od uprawnień użytkownika“). Obiecałem wówczas praktyczny przykład zastosowania. Trochę to potrwało, ale oto i on:). A o rolach danego użytkownika dowiemy się już na samym początku, w Main:
1: CurrentData.Credentials.Username = loginForm.UserName; 2: CurrentData.Credentials.Password = loginForm.Password; 3: 4: string[] roles = ServiceProxyProvider<ISecurityService>.Invoke(x => x.GetRoles()); 5: 6: Thread.CurrentPrincipal = new ProcentPrincipal(new ProcentIdentity(-1, CurrentData.Credentials.Username), roles);
Błędne dane będą skutkowały wyjątkiem na serwerze.
Na specjalną uwagę zasługuje konfiguracja, którą na kliencie dla odmiany trzymam w configu. Jest kilka ważnych elementów:
1) w konfiguracji endpointu w elemencie identity/dns musimy podać wartość wybraną podczas generowania certyfikatu (-n “CN=…”):
1: <endpoint behaviorConfiguration="AnyCertEndpoint" 2: bindingConfiguration="UsernamePassword" 3: address="net.tcp://localhost:28736/Security" 4: binding="netTcpBinding" 5: contract="Procent.Samples.WcfAuthStarter.ServiceContracts.ISecurityService" 6: > 7: <identity> 8: <dns value="ProcentWcfSample" /> 9: </identity> 10: </endpoint>
2) musimy utworzyć odpowiednią konfigurację bindingu dla netTcp:
1: <netTcpBinding> 2: <binding name="UsernamePassword"> 3: <security mode="Message"> 4: <message clientCredentialType="UserName"/> 5: </security> 6: </binding> 7: </netTcpBinding>
3) zakładamy, że nie potrzebujemy w żaden sposób weryfikować certyfikatu serwera:
1: <endpointBehaviors> 2: <behavior name="AnyCertEndpoint"> 3: <clientCredentials> 4: <serviceCertificate> 5: <authentication certificateValidationMode="None"/> 6: </serviceCertificate> 7: </clientCredentials> 8: </behavior> 9: </endpointBehaviors>
W takiej konfiguracji aplikacja… po prostu działa.
Jak wspomniałem wcześniej, aplikacja dostępna na stronie Samples. Czy ktoś stosuje alternatywny sposób do osiągnięcia tego celu? Bardzo chętnie sobie o nim poczytam, proszę o komentarze!
Możesz też użyć narzędzia SelfCert (http://www.pluralsight-training.net/community/blogs/keith/archive/2009/01/22/create-self-signed-x-509-certificates-in-a-flash-with-self-cert.aspx), które umożliwia zachowanie certyfikatu w Trusted Store. Później w WebConfigu możesz użyć:
<serviceCertificate findValue="MojCertyfikat" storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName"/>
Cały wpis znajduje się w:
http://codebetter.com/blogs/peter.van.ooijen/archive/2010/03/22/a-simple-wcf-service-with-username-password-authentication-the-things-they-don-t-tell-you.aspx
Pozdrawiam
albo tak (przynajmniej w .NET4):
private static X509Certificate2 CreateSelfSignedCertificate(string name, string password, DateTime validityFrom, DateTime validityTo)
{
var selfSignedCertificateType = typeof(SecurityBindingElement).Assembly.GetType("System.ServiceModel.Channels.SelfSignedCertificate");
var createMethod = selfSignedCertificateType.GetMethod(
"Create", new[]
{
typeof(string), // name
typeof(string), // password
typeof(DateTime), // start
typeof(DateTime), // expire
typeof(string), // containerName
}
);
var selfSignedCertificate = createMethod.Invoke(
null, // is static,
new object[]
{ // arguments
name,
password,
validityFrom,
validityTo,
Guid.NewGuid().ToString() // containerName
}
);
return (X509Certificate2)selfSignedCertificateType.GetField("x509Cert", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(selfSignedCertificate);
}