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

 

Figura1

 

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…".

Figura2

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.

Figura3

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

Figura4

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

 
x