fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
10 minut

WCF Auth Starter – zalążek aplikacji klient-serwer z uwierzytelnianiem username/password


01.03.2010

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!

0 0 votes
Article Rating
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Krzysztof Jeske
14 years ago

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

Ala Ma Kota
Ala Ma Kota
14 years ago

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);
}

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również