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!

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
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?