Windows Identity Framework – Scenari attivi con servizi WCF
Scritto da
Luca Cestola
il
mercoledì 7 dicembre 2011
Linguaggio:
•
Framework:
•
Livello: 200
Negli articoli precedenti abbiamo visto come implementare un STS
ed utilizzarlo in uno scenario passivo, ovvero quando il client che
richiede l'accesso è un semplice browser.
In uno scenario passivo l'utilizzo del STS per le generazione
del secure token è un processo trasparente per il client (browser)
che si vede semplicemente consegnato un cookie con il token. Questo
scenario va benissimo nel caso in cui il nostro Relyng Party (RP)
sia un sito web che espone le funzionalità tramite pagine
web. Cosa succede invece se uno o più RP espongono dei servizi wcf
che vogliamo richiamara da uno smart client, come può essere ad
esempio un'applicazione WPF o Silverlight?
È questo il caso di uno scenario cosiddetto "attivo", nel quale
la fase di handshaking e scambio delle credenziali avviene
direttamente da parte del client verso il STS.
Anche questa volta facciamoci aiutare da Visual Studio per
ottenere con poco sforzo un webservice ed un STS perfettamente
funzionanti ed integrati.
Avviamo Visual Studio 2010 come amministratori e cominciamo con
il creare il progetto che ospiterà il web service. Scegliamo di
creare un nuovo web site e scegliamo dall'elenco dei template la
voce "Claims-aware WCF Service".

Questo template crea un servizio di esempio pronto all'uso e
fornisce un web.config già preconfigurato per facilitare
l'integrazione con un STS. Vediamone le parti salienti.
Viene fatto riferimento all'assembly
Microsoft.IdentityModel.
<assemblies>
<add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</assemblies>
Viene aggiunta un'estensione che dota la pipeline di wcf
della capacità di integrarsi con un STS.
<extensions>
<behaviorExtensions>
<!-- This behavior extension will enable the service host to be Claims aware -->
<add name="federatedServiceHostConfiguration" type="Microsoft.IdentityModel.Configuration.ConfigureServiceHostBehaviorExtensionElement, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</behaviorExtensions>
</extensions>
Nel behavior associato al servizio di test viene aggiunto un
riferimento all'estensione appena definita. L'estensione agisce a
livello di configurazione del servizio aggiungendo tutto ciò che è
necessario per gestire l'integrazione con il STS sostituendo, ad
esempio, l'authorization manager del servizio con
IdentityModelServiceAuthorizationManager.
<behaviors>
<serviceBehaviors>
<behavior name="ClaimsAwareService1.ServiceBehavior" >
<!-- Behavior extension to make the service claims aware -->
<federatedServiceHostConfiguration/>
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
A questo punto il nostro servizio è pronto per essere integrato
con un STS. Il bindind del servizio è ancora quello creato dal
template ovvero "wsHttpBinding".
Aggiungiamo quindi l'STS utilizzando il wizard che Visual Studio
mette a disposizione tramite il menù contestuale del progetto.
Facciamo quindi click con il tasto destro sul nodo del progetto nel
solution explorer e selezioniamo "Add STS reference…".

Il wizard in questione, lo abbiamo già visto all'opera nello
scenario passivo, ma in questo caso con una piccola, ma sostanziale
differenza. La presenza dell'extension
"federatedServiceHostConfiguration" nel web.config infatti fa sì
che il wizard si accorga che l'oggetto dell'integrazione con il STS
è un web service e ci presenta quindi una schermata aggiuntiva
nella quale è possibile scegliere il contract e l'implementazione
del servizio che utilizzarà il Secure Token per garantire l'accesso
da parte dei client.

Fino a questo punto possiamo cliccare su "Next" lasciando
le impostazioni di default. Nella sezione relativa al STS scegliamo
invece l'opzione "Create a new STS project in current solution",
per far sì che venga creato un STS ad-hoc nella solution ed infine
clicchiamo il pulsante "Finish" sul sommario finale.
Il wizard avrà aggiunto, a questo punto, un nuovo website
contenente il STS e modificato il web.config del RP contenente il
nostro servizio di test.
In particolare il wizard ha provveduto a modificare il tipo di
binding, passando da wsHttpBinding a
ws2007FederationHttpBinding.
<ws2007FederationHttpBinding>
<binding name="ClaimsAwareService1.IService_ws2007FederationHttpBinding">
<security mode="Message">
<message>
<issuerMetadata address="http://localhost:49701/ClaimsAwareService1_STS/Service.svc/mex" />
<claimTypeRequirements>
<!--Following are the claims offered by STS 'http://localhost:49701/ClaimsAwareService1_STS/Service.svc'. Add or uncomment claims that you require by your application and then update the federation metadata of this application.-->
<add claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" isOptional="true" />
<add claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" isOptional="true" />
</claimTypeRequirements>
</message>
</security>
</binding>
</ws2007FederationHttpBinding>
Nella configurazione possiamo vedere che la security è impostata
a livello di messaggio e che viene fornito l'indirizzo dei
metadatai del STS e le claim che verranno richieste al STS.
Un ultimo elemento di configurazione registra
l'associazione tra il RP e l'endpoint del web service del STS.
Se eseguiamo il progetto e navighiamo sui metadati dl servizio
http://.../ClaimsAwareService1/Service.svc?wsdl
possiamo notare nell'xml la presenza della sezione
"EndorsingSupportingTokens" che contiene i riferimenti al STS e
l'elenco dei Claim presenti nella configurazione del nuovo
binding.
Testiamo il servizio
Aggiungiamo ora alla solution un nuovo progetto di tipo "Console
Application" che chiameremo Test tramite il quale consumeremo il
servizio. In fase di creazione del progetto scegliamo di
indirizzare la versione full del frame work evitando il Client
Profile. Tramite il consueto menù contestuale del progetto appena
creato selezioniamo la voce "Add Service Reference".

Cliccando sul pulsante "Discover" otterremo un elenco di
endpoint presenti nei progetti della solution e selezioniamo quello
relativo al servizio di test "ClaimsAwareService1/Service.svc".
Al termine potremo inserire il seguente codice nel metodo
Main.
ServiceReference1.ServiceClient client = new ServiceReference1.ServiceClient();
string result = client.GetData(1);
Console.WriteLine("Rusult-> {0}", result);
Console.ReadLine();
Eseguendo il programma, potremo notare che dopo il servizio
viene correttamente richiamato e la fase di handshaking verso il
STS è completamente trasparente.
|
N.B.
Il wizard predispone, verso il SST, un'autenticazione di tipo
windows pertanto dovremo cambiare il metodo di autenticazione
sull'endpoint del STS se vogliamo utilizzare un altro sistema.
|
Ora che il servizio è funzionante ed integrato con il STS,
sottolineiamo un aspetto molto importante. Con il codice attuale,
la creazione di un'ulteriore istanza del client ed il conseguente
richiamo del metodo del web service darebbe origine ad una nuova
richiesta ed un nuovo hand-shaking verso il STS. Questo accadde
perché per default WCF mantiene il token a livello di Channel del
client, pertanto il token è rutilizzato per diverse chiamate solo
se utilizziamo la stessa istanza del client.
Poiché la fase di hand-shaking messa in atto dall'extension di
WCF risulta molto dispendiosa ed è probabile che si voglia poter
ricreare un'istanza di client riutilizzando un token già
rilasciato, è necessario poter conservare il token che viene
generato la prima volta al fine di riutilizzarlo nelle chiamate
successive con client diversi.
Per fare questo dobbiamo ridefinire la classe ClientCredentials
in modo tale da poter specificare un token differente. Vediamo
l'implementazione di una cache per i token.
Aggiungiamo i riferimenti agli assembly Microsoft.IdentitiModel
e System.IdentityModel (nel prossimo frame work 4.5 ci dovrebbe
essere solamente il secondo). Il codice illustrato è scaricabile qui
ed è un estratto dei sorgenti del progetto http://litwarehr.codeplex.com.
Analizziamo alcuni tratti fondamentali.
Viene creata una classe CustomClientCredential il cui unico
scopo è quello di ridefinire il metodo virtuale
CreateSecurityTokenManager.
/// <summary>
/// Custom implementation
/// </summary>
class CustomClientCredentials : ClientCredentials
{
public CustomClientCredentials()
: base()
{
}
protected CustomClientCredentials(ClientCredentials other)
: base(other)
{
}
protected override ClientCredentials CloneCore()
{
return new CustomClientCredentials(this);
}
/// <summary>
/// Returns a custom security token manager
/// </summary>
/// <returns></returns>
public override System.IdentityModel.Selectors.SecurityTokenManager CreateSecurityTokenManager()
{
return new CustomClientCredentialsSecurityTokenManager(this);
}
}
La classe CustomClientCredentialsSecurityTokenManager eredita
dal ClientCredentialsSecurityTokenManager che ha il compito di
gestire il token del client. In essa viene definito un dictionary
che rappresenta la cache.
class CustomClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
{
private static Dictionary<Uri, CustomIssuedSecurityTokenProvider> providers = new Dictionary<Uri, CustomIssuedSecurityTokenProvider>();
public CustomClientCredentialsSecurityTokenManager(ClientCredentials credentials)
: base(credentials)
{
}
/// <summary>
/// Returns a custom token provider when a issued token is required
/// </summary>
public override System.IdentityModel.Selectors.SecurityTokenProvider CreateSecurityTokenProvider(System.IdentityModel.Selectors.SecurityTokenRequirement tokenRequirement)
{
if (this.IsIssuedSecurityTokenRequirement(tokenRequirement))
{
IssuedSecurityTokenProvider baseProvider = (IssuedSecurityTokenProvider)base.CreateSecurityTokenProvider(tokenRequirement);
CustomIssuedSecurityTokenProvider provider = new CustomIssuedSecurityTokenProvider(baseProvider);
return provider;
}
else
{
return base.CreateSecurityTokenProvider(tokenRequirement);
}
}
}
Questo token manager ridefinisce il metodo
"CreateSecurityTokenProvider" che ha il compito di fornire
l'istanza di tipo "IssuedSecurityTokenProvider" che rappresenta il
token vero e proprio. Anche questa viene ridefinita per poter
ridefinire il suo metodo "GetTokenCore" al fine di inserire o
recuperare il token dalla cache.
class CustomIssuedSecurityTokenProvider : IssuedSecurityTokenProvider
{
private IssuedSecurityTokenProvider innerProvider;
/// <summary>
/// Constructor
/// </summary>
public CustomIssuedSecurityTokenProvider(IssuedSecurityTokenProvider innerProvider)
: base()
{
this.innerProvider = innerProvider;
this.CacheIssuedTokens = innerProvider.CacheIssuedTokens;
this.IdentityVerifier = innerProvider.IdentityVerifier;
this.IssuedTokenRenewalThresholdPercentage = innerProvider.IssuedTokenRenewalThresholdPercentage;
this.IssuerAddress = innerProvider.IssuerAddress;
this.IssuerBinding = innerProvider.IssuerBinding;
foreach (IEndpointBehavior behavior in innerProvider.IssuerChannelBehaviors)
{
this.IssuerChannelBehaviors.Add(behavior);
}
this.KeyEntropyMode = innerProvider.KeyEntropyMode;
this.MaxIssuedTokenCachingTime = innerProvider.MaxIssuedTokenCachingTime;
this.MessageSecurityVersion = innerProvider.MessageSecurityVersion;
this.SecurityAlgorithmSuite = innerProvider.SecurityAlgorithmSuite;
this.SecurityTokenSerializer = innerProvider.SecurityTokenSerializer;
this.TargetAddress = innerProvider.TargetAddress;
foreach (XmlElement parameter in innerProvider.TokenRequestParameters)
{
this.TokenRequestParameters.Add(parameter);
}
this.innerProvider.Open();
}
/// <summary>
/// Gets the security token
/// </summary>
/// <param name="timeout"></param>
/// <returns></returns>
protected override System.IdentityModel.Tokens.SecurityToken GetTokenCore(TimeSpan timeout)
{
SecurityToken securityToken = null;
if (this.CacheIssuedTokens)
{
securityToken = TokenCache.GetToken(this.innerProvider.IssuerAddress.Uri);
if (securityToken == null || !IsServiceTokenTimeValid(securityToken))
{
securityToken = innerProvider.GetToken(timeout);
TokenCache.AddToken(this.innerProvider.IssuerAddress.Uri, securityToken);
}
}
else
{
securityToken = innerProvider.GetToken(timeout);
}
return securityToken;
}
/// <summary>
/// Checks the token expiration.
/// A more complex algorithm can be used here to determine whether the token is valid or not.
/// </summary>
private bool IsServiceTokenTimeValid(SecurityToken serviceToken)
{
return (DateTime.UtcNow <= serviceToken.ValidTo.ToUniversalTime());
}
~CustomIssuedSecurityTokenProvider()
{
this.innerProvider.Close();
}
}
Questa implementazione gestisce anche la richiesta di rinnovo di
un token il cui lease sia scaduto.
Nel web.config aggiungiamo la segente configurazione nella
sezione system.ServiceModel, per far si che il nostro client possa
usufruire dei meccanismi di cache.
<behaviors>
<endpointBehaviors>
<behavior>
<clientCredentials type="Test.CustomClientCredentials, Test" />
</behavior>
</endpointBehaviors>
</behaviors>
A questo punto possiamo modificare il metodo Main del nostro
programma come segue:
ServiceReference1.ServiceClient client = new ServiceReference1.ServiceClient();
string result = client.GetData(1);
Console.WriteLine("Rusult-> {0}", result);
client = new ServiceReference1.ServiceClient();
result = client.GetData(2);
Console.WriteLine("Rusult-> {0}", result);
Console.ReadLine();
Se si attiva un breakpoint sul metodo "GetOutputClaimsIdentity"
della classe "CustomSecurityTokenService" presente nel progetto del
STS vedremo che il metodo viene richiamato solamente la prima volta
che invochiamo il metodo GetData del web service, anche se la
seconda chiamata avviene tramite una seconda istanza del
client.
|
N.B.
Quanto descritto nell'articolo è applicabile ad applicazioni che
facciano uso della versione full (non Client Profile) del frame
work .net e non è quindi applicabile a Silverlight. Ad ogni modo
con l'SDK rilasciato ad aprile 2011 è stata rilasciata una libreria
che implementa un subset di WIF proprio per la gestione di scenari
attivi. È auspicabile che con il tempo questo codice entri a far
parte del frame work.
|
Conclusioni
Gli scenari attivi sono di norma più complessi di quelli
passivi, ma è comunque apprezzabile l'apporto di WIF nel
semplificare in buona parte i meccanismi. WIF è un complemento di
WCF che è, e resta, un framework molto ampio la cui totale
conoscenza è un obbiettivo piuttosto "sfidante". C'è comunque
spazio per una maturazione di queste tecnologie ed una ulteriore
semplificazione dei diversi scenari.
Tags: WIF