"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!