WCF Custom Authentication and Impersonation
Nell'ultimo periodo ho avuto l'esigenza di sviluppare un sistema di Authentication e Impersonation personalizzato per WCF (Indigo). Non è stato banae perché non esiste ancora molta documentazione a riguardo, d'altra parte siamo ancora in beta, quindi è legittimo. In ogni caso sono riuscito nel mio intento e riporto in queste poche pagine la strada che ho percorso, per agevolare altri che eventualmente ne avessero bisogno.
Ovviamente io sono solo un appassionato e innamorato utilizzatore di WCF, anche per esigenza visto che lo sto utilizzando in un progetto reale di un mio cliente, ma se qualcuno (eventualmente del team di WCF...) volesse lasciare qualche commento o precisazione a proposito di quanto sto scrivendo ... sarebbe ben accetto e gradito. Grazie.
WCF supporta diversi meccanismi di autenticazione, di seguito è riportato un file di configurazione di esempio di un host WCF:
<?
xml version="1.0" encoding="utf-8" ?>
<
configuration>
<system.serviceModel>
<services>
<service type="DevLeap.Indigo.MyService, DevLeap.Indigo" behaviorConfiguration="MyServiceBehavior">
<endpoint address="net.tcp://localhost:35000/Services/MyService/" binding="netTcpBinding"
bindingConfiguration="MyServiceBinding" contract="DevLeap.Indigo.IMyService, DevLeap.Indigo" />
</service>
</services>
<bindings>
<netTcpBinding>
<binding name="MyServiceBinding">
<security mode="Message">
<message clientCredentialType="UserName" defaultProtectionLevel="EncryptAndSign" />
</security>
</binding>
</netTcpBinding>
</bindings>
<behaviors>
<behavior name="MyServiceBehavior" returnUnknownExceptionsAsFaults="True">
<metadataPublishing enableGetWsdl="true" />
<serviceAuthorization principalPermissionMode="Custom" />
<!-- UseAspNetRoles -->
<serviceCredentials>
<serviceCertificate findValue="localhost" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" />
<userNamePassword membershipProviderName="MyMembershipProvider" />
</serviceCredentials>
</behavior>
</behaviors>
</system.serviceModel>
<
system.web>
<membership>
<providers>
<add name="MyMembershipProvider" type="DevLeap.WSSecurity.MyMembershipProvider, DevLeap.WSSecurity" />
</providers>
</membership>
</system.web>
</
configuration>
Occorre prestare particolare attenzione alla sezione message, all'interno del binding di tipo netTcpBinding denominato MyServiceBinding. Ho dichiarato che utilizzerò credenziali (clientCredentialType) di tipo "UserName". Significa che l'utilizzatore del servizio dovrà fornire uno UsernameToken al servizio, per essere autenticato.
Successivamente nel file di configurazione, nalle sezione dei behaviors, ho dichiarato un behavior personalizzato di nome MyServiceBehavior, nel quale ho definito come intendo gestire lo UsernameToken, per ottenere un IPrincipal per l'utente corrente, configurando l'attributo principalPermissionMode dell'elemento
serviceAuthorization. I valori possibili - suggeriti anche dall'intellisense - sono:
- None: non c'è bisogno di spiegarlo.
- UseWindowsGroups: utilizza gli utenti di Windows e si basa su WindowsPrincipal e WindowsIdentity
- UseAspNetRoles: si appoggia ad un ASP.NET 2.0 MembershipProvider, lavora con implementazioni di IIdentity e IPrincipal
- Custom: meccanismo personalizzato (quello che ci accingiamo ad utilizzare noi)
Il file di configurazione riporta sia la configurazione Custom che, commentata, la configurazione per utilizzare un MembershipProvider, qualora servisse a qualcuno (si veda userNamePassword nella configurazione del behavior).
Se si definisce un principalPermissionMode di tipo Custom occorre definire e configurare una implementazione personalizzata di ServiceAuthorizationBehavior. Ecco come è possibile farlo via codice:
using (ServiceHost host = new ServiceHost(typeof(MyService)))
{
ServiceAuthorizationBehavior sa = host.Description.Behaviors.Find<ServiceAuthorizationBehavior>();
sa.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
sa.AuthorizationDomain = new AuthorizationDomain(new IAuthorizationPolicy[] { new DevLeap.WSSecurity.DevLeapAuthorizationPolicy() });
ServiceCredentials original = host.Description.Behaviors.Remove<ServiceCredentials>();
host.Description.Behaviors.Add(new DevLeap.WSSecurity.DevLeapServiceCredentials(original, null));
host.Open();
Console.WriteLine("Host listening ...");
Console.ReadLine();
}
Innanzitutto occorre ottenere un riferimento al ServiceAuthorizationBehavior dell'host, quindi va dichiarato un AuthorizationDomain personalizzato. Un AuthorizationDomain è costituito da un insieme di classi che implementano IAuthorizationPolicy, interfaccia definita in System.Security.Authorization di .NET 2.0. IAuthorizationPolicy è l'interfaccia base di .NET per definire Claims personalizzati. Ad esempio la classe DevLeapAuthorizationPolicy definisce il fatto che vogliamo associare gli utenti a dei nostri IPrincipal personalizzati, basati sulle credenziali dell'utente autenticato. Eccone il codice:
using
System;
using System.Collections.Generic;
using System.Text;
using System.Security.Authorization;
using System.Security.Principal;
using DevLeap.Security;
namespace
DevLeap.WSSecurity
{
public class DevLeapAuthorizationPolicy : IAuthorizationPolicy
{
string id = Guid.NewGuid().ToString();
public string Id
{
get { return this.id; }
}
public ClaimSet Issuer
{
get { return ClaimSet.Anonymous; }
}
public bool Evaluate(EvaluationContext context, ref object state)
{
object primaryIdentity;
if (!context.Properties.TryGetValue("PrimaryIdentity", out primaryIdentity)) return false;
context.Properties["Principal"] = new DevLeapPrincipal(new DevLeapIdentity(((IIdentity)primaryIdentity).Name));
return true;
}
}
}
La parte maggiormente significativa dell'implementazione personalizzata di IAuthorizationPolicy è nel metodo Evaluate. Utilizzando il contesto (context) del Claim in fase di valutazione, tento di ottenere il riferimento ad una proprietà di nome PrimaryIndentity. Questa proprietà rappresenta l'identità definita da WCF durante la verifica delle credenziali utente. Se non ho a disposizione la PrimaryIdentity significa che l'utente non è stato correttamente identificato, quindi il Claim fallisce. Altrimenti si converte la PrimaryIdentity, che non è altro che una implementazione di IIdentity (in realtà è una GenericIdentity), in una DevLeapIdentity personalizzata, quindi definisco un DevLeapPrincipal personalizzato, sulla base della DevLeapIdentity, e lo salvo nella proprietà Principal del contesto corrente (context). WCF si occupera di rileggere questo Principal dal contesto, impersonificandolo durante la chiamata al servizio, nell'esecuzione del codice delle sue operazioni.
Infatti se proviamo a verificare il tipo .NET e il valore degli IPrincipal e IIdentity associati con il thread corrente, durante l'esecuzione di una operazione del servizio, otterremo i nostri DevLeapPrincipal e DevLeapIdentity:
Console.WriteLine(System.Threading.Thread.CurrentPrincipal.GetType().Name);
Console.WriteLine(System.Threading.Thread.CurrentPrincipal.Identity.GetType().Name);
Console.WriteLine(System.Threading.Thread.CurrentPrincipal.Identity.Name);
Console.WriteLine(OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.GetType().Name);
Console.WriteLine(OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name);
Ecco cosa viene scritto sulla finestra Console di Windows, se mi autentico come utente "PaoloPia":
DevLeapPrincipal
DevLeapIdentity
PaoloPia
GenericIdentity
PaoloPia
L'ultima porzione di codice da definire è quella che dichiara come autenticare lo UserNameToken al fine di avere poi una PrimaryIdentity che corrisponda alle credenziali dell'utente. Poche righe più in alto, nel codice del ServiceHost, abbiamo definito anche:
ServiceCredentials original = host.Description.Behaviors.Remove<ServiceCredentials>();
host.Description.Behaviors.Add(new DevLeap.WSSecurity.DevLeapServiceCredentials(original, null));
Questo codice si limita a referenziare una implementazione della classe ServiceCredentials, aggiungendola alla configurazione del ServiceHost. L'implementazione predefinita della classe ServiceCredentials presente in System.ServiceModel deriva da System.ServiceModel.Security.SecurityCredentialsManager e definisce i seguenti meccanismi di autenticazione: ClientCertificate, ServiceCertificate, UserNamePassword e Windows. ServiceCredentials, all'interno del suo metodo CreateTokenAuthenticator, decide quale SecurityTokenAuthenticator utilizzare, per validare il token fornito dal consumer del servizio. Ogni SecurityTokenAuthenticator non è altro che una classe derivata da SecurityTokenAuthenticator che verifica un particolare token. WCF ad oggi definisce due diverse tipologie di UserNameSecurityTokenAuthenticator:
- WindowsUsernNamePasswordTokenAuthenticator: utilizza il database utenti di Windows per validare il token
- MembershipUserNamePasswordTokenAuthenticator: utilizza ASP.NET 2.0 MembershipProvider API per validare il token
Ecco la nostra implementazione personalizzata di ServiceCredentials, per utilizzare un database personalizzato di utenti, per valdiare il token:
using
System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;
namespace
DevLeap.WSSecurity
{
public class DevLeapServiceCredentials: ServiceCredentials
{
public DevLeapServiceCredentials(ServiceCredentials original, params string[] trustedSecurityTokenServices)
{
Type scType = typeof(ServiceCredentials);
scType.GetField("userName", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, original.UserNamePassword);
scType.GetField("clientCertificate", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, original.ClientCertificate);
scType.GetField("serviceCertificate", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, original.ServiceCertificate);
scType.GetField("windows", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, original.Windows);
}
protected override SecurityTokenAuthenticator CreateTokenAuthenticator(SecurityTokenParameters parameters)
{
return (new DevLeapUserNamePasswordTokenAuthenticator());
}
}
}
All'interno del costruttore eseguiamo via System.Reflection, una copia della configurazione dell'implementazione originale di ServiceCredentials, sulla nostra stessa configurazione, poi ridefiniamo il metodo CreateTokenAuthenticator per restituire il nostro personalizzato. Ecco come:
using
System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;
using System.Security.Authorization;
using System.Collections.ObjectModel;
using DevLeap.Security;
namespace
DevLeap.WSSecurity
{
public class DevLeapUserNamePasswordTokenAuthenticator: UserNameSecurityTokenAuthenticator
{
private class DevLeapUserNamePasswordToken: UserNameSecurityToken
{
public DevLeapUserNamePasswordToken(string userName, string password): this(userName, password, SecurityToken.GenerateId())
{ }
public DevLeapUserNamePasswordToken(string userName, string password, string id): base(userName, password, id)
{ }
protected override void ValidateCore(SecurityTokenResolver tokenResolver)
{
if (DevLeapSecurity.AuthenticationProvider.ValidateUser(base.UserName, base.Password) <= 0)
{
throw new SecurityTokenValidationException("UserNamePasswordTokenAuthenticationFailed");
}
}
}
public override SecurityToken CreateUserNamePasswordToken(string id, string userName, string password)
{
UserNameSecurityToken token = null;
if (id == null)
token = new DevLeapUserNamePasswordTokenAuthenticator.DevLeapUserNamePasswordToken(userName, password);
else
token = new DevLeapUserNamePasswordTokenAuthenticator.DevLeapUserNamePasswordToken(userName, password, id);
token.Validate();
return token;
}
}
}
All'interno del metodo CreateUserNamePasswordToken si definisce un'istanza di uno UserNameToken personalizzato (DevLeapUserNamePasswordToken) che ridefinisce il metodo ValidateCore, chiamando un nostro sistema di autenticazione personalizzato.
Il punto fondamentale di questo meccanismo di autenticazione personalizzata è che siamo in grado di invocare i metodi degli oggetti di business, presenti alle spalle dei sevizi SOAP, potendo utilizzare la sicurezza imperativa e dichiarativa nel codice degli oggetti di business, anche se i consumer degli oggetti sono dei client SOAP dei servizi esposti via WCF, tra l'altro a prescindere dal protocollo di binding utilizzato!
Ecco fatto! Spero che serva a qualcuno ....