“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!
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?