Własne mechanizmy uwierzytelniania w WCF

1

"Bezpieczeństwo w WCF" – pojęcie takie wydaje się oklepane i opisane na wszelkie możliwe sposoby. Tyle materiałów, tyle blogów, artykułów, książek…

Chciałem osiągnąć rzecz bardzo prostą, właściwie – podstawową. Zacząłem od stworzenia własnej implementacji interfejsów "tożsamości": IIdentity:

  1:  public class ProcentIdentity : GenericIdentity
  2:  {
  3:  	public int Id;
  4:  
  5:  	public ProcentIdentity(int id, string name)
  6:  		: base(name)
  7:  	{
  8:  		Id = id;
  9:  	}
 10:  
 11:  	public static ProcentIdentity Current
 12:  	{
 13:  		get
 14:  		{
 15:  			return Thread.CurrentPrincipal.Identity as ProcentIdentity;
 16:  		}
 17:  	}
 18:  }

oraz IPrincipal:

  1:  public class ProcentPrincipal : GenericPrincipal
  2:  {
  3:  	public readonly ReadOnlyCollection<string> Roles;
  4:  
  5:  	public ProcentPrincipal(ProcentIdentity identity, string[] roles)
  6:  		: base(identity, roles)
  7:  	{
  8:  		Roles = new ReadOnlyCollection<string>(roles ?? new string[0]);
  9:  	}
 10:  
 11:  	public static ProcentPrincipal Current
 12:  	{
 13:  		get
 14:  		{
 15:  			return Thread.CurrentPrincipal as ProcentPrincipal;
 16:  		}
 17:  	}
 18:  }

Następnie pragnieniem mym było podpiąć je pod aktualne żądanie na serwerze WCF, zakładając uwierzytelnianie za pomocą loginu i hasła. A to żeby mieć łatwy dostęp do zawartych tam informacji, a to żeby skorzystać z PrincipalPermissionAttribute, a to żeby IsInRole() zwracała zdefiniowane przeze mnie uprawnienia, w końcu – bo tak mi sie podobało.

I zaczęły się schody. Można to zapewne zrobić jakimś brzydkim "hakiem", ale mi chodziło o rozwiązanie zgodne z zaleceniami i wykorzystaniem jakże rozszerzalnej architektury WCF.

Kluczem do osiągnięcia celu okazały się dwa, ot, byty zdefiniowane w bibliotece System.IdentityModel.dll. Pierwszy z nich, abstrakcyjna klasa UserNamePasswordValidator, zawiera strukturę służącą do… (surprise!!) walidacji loginu i hasła przekazanych przez użytkownika. Konkretna implementacja do projektu testowego wygląda u mnie tak:

  1:  public override void Validate(string userName, string password)
  2:  {
  3:  		User user = SampleDataAccessForDemoPurposesOnly.Users.GetByUserName(userName);
  4:  
  5:  		if (user == null || user.Password != password)
  6:  			throw new SecurityTokenValidationException();
  7:  }

Drugi ze wspomnianych bytów to interfejs IAuthorizationPolicy. Poprawna implementacja tego z kolei potwora wymagała dość dużo czasu i grzebania się zarówno w internecie jak i reflektorze. Oto ona:

  1:  private Guid _authPolicyId = Guid.NewGuid();
  2:  public string Id
  3:  {
  4:  	get { return _authPolicyId.ToString(); }
  5:  }
  6:  
  7:  public bool Evaluate(EvaluationContext evaluationContext, ref object state)
  8:  {
  9:  	IIdentity identity = ((IList<IIdentity>)evaluationContext.Properties["Identities"]).Single();
 10:  
 11:  	int userId = SampleDataAccessForDemoPurposesOnly.Users.GetByUserName(identity.Name).Id;
 12:  
 13:  	var customIdentity = new ProcentIdentity(userId, identity.Name);
 14:  	string[] roles = SampleDataAccessForDemoPurposesOnly.Users.GetRolesForUser(userId);
 15:  	var customPrincipal = new ProcentPrincipal(customIdentity, roles);
 16:  	evaluationContext.Properties["PrimaryIdentity"] = customIdentity;
 17:  	evaluationContext.Properties["Principal"] = customPrincipal;
 18:  
 19:  	return true;
 20:  }
 21:  
 22:  public ClaimSet Issuer
 23:  {
 24:  	get { return ClaimSet.System; }
 25:  }

Efektem działania powyższego kodu jest podstawienie pod Thread.CurrentPrincipal moich własnych konstrukcji, o co chodziło mi na samym początku. Przyznaję, że nie wygląda to ślicznie, ale… jeśli znasz ładniejszy sposób na osiągnięcie tego samego celu to podziel się proszę ze mną i czytelnikami programistycznym posiłkiem.

Do pełni szczęścia pozostało odpowiednie poinstruowanie WCF, że właśnie tego kodu ma użyć do uwierzytelniania użytkownika. Ja zaimplementowałem powyższe mechanizmy w jednej klasie:

  1:  public class ServerSecurity : UserNamePasswordValidator, IAuthorizationPolicy
  2:  {

i podaję jej instancję w kodzie, podczas otwierania usług na serwerze:

  1:  ServerSecurity security = new ServerSecurity();
  2:  
  3:  NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.Message);
  4:  tcpBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
  5:  host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
  6:  host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = security;
  7:  
  8:  host.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
  9:  host.Authorization.ExternalAuthorizationPolicies = new ReadOnlyCollection<IAuthorizationPolicy>(new[] { security });
 10:  
 11:  host.Open();

To tyle na ten temat. Po przedstawieniu sposobu na wpięcie się w "infrastrukturę bezpieczeństwa WCF" oraz wygodnego sposobu na zdalne wywołanie usług przyjdzie niebawem pora na zaprezentowanie działającego, pokonfigurowanego demka. Bis dann!

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

1 Comment

  1. W metodzie Evaluate (IAuthorizationPolicy) dodanie klucza "PrimaryIdentity" nie ustawia odpowiednio ProcentIdentity. W Twoim przypadku (WcfAuthStarter) wszystko działa bo referencja do tożsamości siedzi w ProcentPrinciple, ale już ServiceSecurityContext.Current.PrimaryIdentity zwróci oryginalne GenericIdentity, a nie ProcentIdentity.

    //ok przez ref w principal
    ProcentIdentity pt = Thread.CurrentPrincipal.Identity as ProcentIdentity;
    //null
    ProcentIdentity ps = ServiceSecurityContext.Current.PrimaryIdentity as ProcentIdentity;

    Żeby wszystko było ok, najlepiej podmienić tożsamości w liście pod kluczem "Identities".

    PS natknąłem się na to kompletnie przypadkiem bo nie potrzebowałem używać własnego IPrincipal w systemie. C
    PS2 Chyba, że to jakieś zamierzone działanie?

Newsletter: devstyle weekly!
Dołącz do 1000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania w najbliższy piątek!
Niech DEV będzie z Tobą!