Windows Identity Framework – Il Secure Token Service

Scritto da  Luca Cestola il martedì 10 maggio 2011
Linguaggio: C#   •  Framework: 4.0   •  Livello: 200

Download pdf


In uno scenario di autenticazione federata, il ruolo del Security Token Service (STS) è decisamente centrale, poiché in esso si concentrano tutte le logiche e gli aspetti tecnologici. In questo articolo vedremo come implementare un STS con WIF e gestire le dinamiche relative all'autenticazione e all'autorizzazione di un'applicazione web.

Facciamo un breve riassunto delle terminologie utilizzate in questo contesto.

  • Claim
    È  un'attestazione fatta nei confronti di un soggetto. Il contenuto può riferirsi a qualsiasi tipo di informazione, come ad esempio il nome, l'indirizzo email, un gruppo di appartenenza, ecc…
  • Relying Party (RP)
    Un'applicazione che si affida alle Claim contenute in un Security Token per stabilire l'identità di chi accede.
  • Security Token (ST)
    È  la sequenza di informazioni che rappresenta le attestazioni (claim) firmate digitalmente dall'ente che le emette. La firma digitale, fornisce ad un Relying Party una prova sicura riguardo l'integrità delle attestazioni e l'identità di chi ha emesso il token.
  • Issuing Authority (IA)
    È l'entità che emette il Security Token.
  • Identity provider (IP)
    È l'entità che fornisce le claim che verranno poi incluse nel Security Token dalla Issuing Authority.
  • Secure Token Service (STS)
    Identifica il software che emette materialmente il Security Token.

Ciò che andiamo ad implementare è un STS, con funzionalità minimali, che servirà ad analizzare come i diversi elementi sono coinvolti all'interno del processo di autenticazione ed autorizzazione. Il nostro STS sarà contenuto in un'applicazione web che rappresenterà anche il nostro Identity Provider (IP) e Issuing Authority (IA).

Come vedremo, la collaborazione tra IP, IA e STS è molto stretta, poiché per poter produrre un Security token, la IA ha bisogno di recuperare le informazioni relative all'utente dal IP ed utilizzare il STS per emettere materialmente il token. Nella solution che stiamo per implementare queste tre compomenti coesistono, per semplicità, nello stesso progetto web.

Creiamo l'STS

Per semplificarci il lavoro, Visual Studio 2010 ci mette a disposizione uno strumento che crea l'infrastruttura di base di un STS. Andremo quindi ad analizzare il codice prodotto per capire i diversi elementi che costituiscono l'STS.

Cominciamo creando un'applicazione web d'esempio. Avviamo Visual Studio 2010 come amministratori  e creiamo un nuovo progetto web che chiameremo MioRP. Il sito creato con il template di default di Visual Studio presenta nel web.config una configurazione per l'autenticazione basata su form con un riferimento alla pagina di login. Con l'introduzione di WIF e del STS, questa pagina non sarà più necessaria.

Utilizziamo il wizard per creare un STS minimale che esamineremo e modificheremo per soddisfare le nostre esigenze. Nel Solution Explorer selezioniamo quindi il progetto web e accediamo al menù contestuale cliccando con il tasto destro per poi selezionare la voce "Add STS reference". Possiamo anche utilizzare l'omonima voce che si trova sotto al menù "Tools".

figura1

A questo punto apparirà il wizard che abbiamo avuto modo di vedere nel precedente articolo, ma questa volta lo utilizzeremo per ottenere la creazione di un STS nella nostra solution.

Il wizard ci chiederà l'ubicazione del nostro web.config e l'url principale nostro sito. Possiamo prendere quest'ultimo dal browser, eseguendo l'applicazione e copiando l'url dalla barra dell'indirizzo del browser o verificando il numero di porta utilizzata dalla sezione Web presente nelle proprietà dell'applicazione.

figura2

Premiamo il pulsante "next" ignorando per il momento l'avviso riguardante il mancato utilizzo del protocollo https. Nella pagina successiva ci viene chiesto se vogliamo collegare la nostra applicazione web ad un STS esistente o se vogliamo che ne venga aggiunto uno alla nostra solution. Selezioniamo quindi la voce "Create a new STS project in the current solution", premiamo il pulsante "Next" e poi "Finish"

Il wizard aggiungerà il sito "MioRP_STS" già preconfigurato per servire il nostro sito ed avrà modificato anche il web.config della nostra applicazione.  Se il nostro sito fa riferimento alla versione 4 del  framework .Net , alle modifiche apportate dal wizard dovremo aggiungerne delle altre per permettere al sito di potersi integrare correttamente con il STS.

Abbiamo già trattato questi passaggi nel precedente articolo, ma li riepilogo per comodità.

Aggiungiamo la seguente configurazione alla sezione system.web del web.config di MioRP:

<httpModules>
  <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  <add name="ClaimsAuthorizationModule" type="Microsoft.IdentityModel.Web.ClaimsAuthorizationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>

 

A causa delle differenti modalità di validazione delle richieste http rispetto alla versione 2.0 del framework dovremo aggiungere al progetto la seguente classe:

public class WIFRequestValidator : RequestValidator
{
    protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
    {
        validationFailureIndex = 0;
 
        if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))
        {
            SignInResponseMessage message = WSFederationMessage.CreateFromFormPost(context.Request) as SignInResponseMessage;
            if (message != null)
            {
                return true;
            }
        }
        return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
    }
}

 

Configuriamo poi Asp.Net per utilizzare la classe WIFRequestValidator aggiungendo la seguente configurazione sempre nel web.config:

 
<httpRuntime requestValidationType="SharedKook.WIFRequestValidator, WIFTest"/>
 

 

Infine, per poter compilare, aggiungiamo al progetto un riferimento all'assembly Microsoft.IdentityModel.

Il processo di autenticazione.

Se guardiamo i file presenti nel sito MioRP_STS, vedremo che non è molto complesso, perché WIF accentra in sé la maggior parte del codice necessario alla gestione dell'autenticazione federata, lasciando a noi il solo compito di aggiungere la logica custom.

Il progetto contiene i seguenti elementi

  • una pagina Default.aspx che fornisce l'entry point del processi di autenticazione tramite il STS
  • una pagina di login tramite la quale inseriremo le nostre credenziali
  • la cartella App_Code contenente alcune classi di supporto

Per spiegare esattamente cosa succede durante il processo di autenticazione proviamo a seguire il percorso che le richieste http fanno attraverso i diversi passaggi. Indirizziamo il browser  verso l'url della nostra applicazione MioRP. L'applicazione, che abbiamo precedentemente configurato, tramite il modulo WSFederationAuthenticationModule controlla che nei cookie associati alla richiesta http vi sia un token e che sia valido. La validità viene verificata controllando che il token sia firmato dal giusto STS. Questa verifica è resa possibile, in maniera a noi trasparente, grazie agli HttpModules aggiunti al web.config ed alla seguente sezione di configurazione.

<federatedAuthentication>
  <wsFederation passiveRedirectEnabled="true" issuer="http://localhost:1088/MioRP_STS/" realm="http://localhost:1082" requireHttps="false" />
  <cookieHandler requireSsl="false" />
</federatedAuthentication>

 

L'attributo wsFederation contiene le coordinate degli elementi che sono coinvolti nella relazione di fiducia. L'attributo passiveRedirectEnabled specifica che il modulo WSFederationAuthenticationModule, a seguito di una verifica negativa sul token, reindirizzerà autonomamente il browser verso l'url che rappresenta il STS.

Eseguendo l'applicazione MioRP il browser proverà ad accedere alla pagina di default del sito. I moduli WIF aggiunti si occuperanno di verificare la presenza di un ST valido. Ovviamente la prima volta non siamo in possesso di questo token e pertanto il browser viene redirezionato  verso l'url contenuto nell'attributo issuer del tag wsFederation. L'url  verso il quale si viene redirezionati, contiene una serie di parametri, che serviranno al servizio STS per soddisfare la richiesta ed identificare il Relying Party interessato. A questo punto la richiesta viene presa in carico dal sito che ospita il servizio STS e che rappresenta la nostra IA.

Poiché il sito in questione ha un'autenticazione di tipo Forms, il browser verrà rediretto verso la pagina di login ed i parametri originali della richiesta codificati nel campo returnurl.

http://localhost:1088/SharedBooks_STS/Login.aspx?ReturnUrl=%2fSharedBooks_STS%2fdefault.aspx...

La maschera di login richiede il nome utente ed una password. Ovviamente questi dati sono solo un esempio ed in un caso reale potremmo chiedere altri dati, come ad esempio un ulteriore codice di sicurezza, prodotto magari da un token hardware.

Inserendo i dati e premendo il pulsante di submit, la pagina di login elaborerà i dati inseriti ed in caso di esito positivo consentirà la richiesta. Il codice in questione è il seguente:

protected void Page_Load( object sender, EventArgs e )
{
    // Note: Add code to validate user name, password. This code is for illustrative purpose only.
    // Do not use it in production environment.
    if ( !string.IsNullOrEmpty( txtUserName.Text ) )
    {
        if ( Request.QueryString["ReturnUrl"] != null )
        {
            FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, false );
        }
        else
        {
            FormsAuthentication.SetAuthCookie( txtUserName.Text, false );
            Response.Redirect( "default.aspx" );
        }
    }
    else if ( !IsPostBack )
    {
        txtUserName.Text = "Adam Carter";
    }
}

 

Essendo solo un esempio manca completamente la logica di controllo delle credenziali, che potrà essere implementata secondo le specifiche esigenze, pertanto l'accesso risulta sempre positivo a meno che non sia presente il nome dell'utente. I metodi statici RedirectFromLoginPage e SetAuthCookie della classe FormsAuthentication sono praticamente equivalenti con la sola differenza che il primo utilizzerà il contenuto del parametro returnurl della QueryString, per reindirizzare il browser verso la richiesta originale contenenete i dati utili al STS.

È da notare che fino a questo momento il STS non è stato assolutamente ancora chiamato in causa. La verifica delle credenziali è una questione assolutamente indipendente in uno scenario passivo poiché il servizio STS non è esposto verso l'esterno ma viene utilizzato internamente dalla Issuing Authority per produrre il token. Inoltre è bene comprendere che la modalità di autenticazione può avvenire con qualsiasi modalità. Potremmo decidere, ad esempio, di utilizzare la windows authentication per consentire la verifica delle credenziali su un dominio.

Proseguiamo il viaggio. A questo punto siamo autenticati e la pagina di login ci ridireziona verso l'url inizialmente richiesto dell'Identity Provider. L'url contiene una serie di parametri che saranno  utili al nostro STS per capire l'azione da intraprendere. Vediamo quali sono:

wa

wsignin1.0

Wtrealm

http://localhost:1082

Wtcx

rm=0&id=passive&ru=%2fdefault.aspx%3f

Wct

2011-09-29T17:40:22Z

 

I parametri in questione non sono significativi solo all'interno del mondo .net, ma fanno riferimento ad una specifica relativa allo standard WS-Federation.

Il parametro "wa" rappresenta l'azione che si richiede al STS. In questo frangente la richiesta è quella di effettuare l'accesso. Questo parametro può assumere anche il valore di wsignout1.0 che indica al STS che il si vuole effettuare,appunto, un sign-out . Questo ultimo caso è utile per implementare un Single Sign Out. Anche se normalmente ai Security Token viene assegnata una validità temporale limitata, un sign out ha un ruolo importante in termini di sicurezza perché impedisce che il browser possa accedere al Relying Party anche se il Security Token è ancora valido da un punto di vista temporale.

I parametri "wtrealm" e "wctx" rappresentano rispettivamente l'indirizzo del Relying Party ed i parametri che verranno passati al Relying Party, mentre il "wct" rappresenta il lesae, ovvero il la validità temporale del token.

Analizziamo ora il codice prodotto dal wizard, relativo al STS. Nel metodo Page_PreRender della pagina Default.aspx del progetto abbiamo il seguente codice.

protected void Page_PreRender( object sender, EventArgs e )
{
    string action = Request.QueryString[WSFederationConstants.Parameters.Action];
    try
    {
        if ( action == WSFederationConstants.Actions.SignIn )
        {
            // Process signin request.
            SignInRequestMessage requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
            if ( User != null && User.Identity != null && User.Identity.IsAuthenticated )
            {
                SecurityTokenService sts = new CustomSecurityTokenService( CustomSecurityTokenServiceConfiguration.Current );
                SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest( requestMessage, User, sts );
                FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse( responseMessage, Response );
            }
            else
            {
                throw new UnauthorizedAccessException();
            }
        }
        else if ( action == WSFederationConstants.Actions.SignOut )
        {
            // Process signout request.
            SignOutRequestMessage requestMessage = (SignOutRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
            FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest( requestMessage, User, requestMessage.Reply, Response );
        }
        else
        {
            throw new InvalidOperationException(
                String.Format( CultureInfo.InvariantCulture,
                                "The action '{0}' (Request.QueryString['{1}']) is unexpected. Expected actions are: '{2}' or '{3}'.",
                                String.IsNullOrEmpty(action) ? "<EMPTY>" : action,
                                WSFederationConstants.Parameters.Action,
                                WSFederationConstants.Actions.SignIn,
                                WSFederationConstants.Actions.SignOut ) );
        }
    }
    catch ( Exception exception )
    {
        throw new Exception( "An unexpected error occurred when processing the request. See inner exception for details.", exception );
    }
}

 

Vediamo che come prima cosa viene acquisito il valore del parametro "wa" che serve a capire se vogliamo effettuare un sign-in o un sign-out. Il valore viene quindi confrontato con le costanti presenti  nella classe WSFederationConstants.Action nei campi SignIn e SignOut.

SignIn

Nel caso di sign-in viene creata un'istanza della classe SignInRequestMessage a partire dall'url contenuto nella richiesta http. Questa classe offre un accesso strutturato per tutte le richieste indirizzate al STS. A questo punto entra in gioco il STS, di cui analizzeremo il funzionamento a breve. Viene infatti creata un'istanza del servizio e richiamato il metodo ProcessSignInRequest della classe FederatedPassiveSecurityTokenServiceOperations  che accetta come parametro l'istanza di SignInRequestMessage. Quello che si ottiene in output è un'istanza della classe SignInResponseMessage che rappresenta la risposta del STS e che contiene il Security Token.

Infine la classe FederatedPassiveSecurityTokenServiceOperations provvede alla reindirizzamento del browser verso il Relyng Party. Questo avviene nel metodo ProcessSignInResponse che riceve in ingresso l'stanza di SignInResponseMessage appena prodotta e l'istanza di HttpResponse della HttpRequest corrente.

SignOut

Nel caso di sign-out invece viene creata l'istanza di SignInRequestMessage e viene subito elaborata dal metodo ProcessSignOutRequest  delle classe FederatedPassiveSecurityTokenServiceOperations.

In un contesto reale il sign-out prevederebbe delle azioni aggiuntive. Poiché il Security Token prodotto potrebbe ancora avere un lease valido. L'IP dovrebbe comunicare al Relying Party l'invalidazione dello specifico Token in modo che il RP possa negare l'accesso tramite esso.

Come funziona il STS

Vediamo ora come è organizzatoo il codice che implementa il STS. Nella cartella App_Code del sito SharedBook_STS troviamo quattro classi. La classe CertificateUtils fornisce un metodo semplificato per il recupero di un Certificato Digitale, tramite tre semplici parametri:

  • StoreLocation:
    Indica se l'archivio dei certificati dove effettuare la ricerca è quello dell'utente con il quale il gira il processo che fa la richiesta oppure se è quello a livello globale del sistema operativo.
  • StoreName:
    Indica una suddivisione in categorie dei certificati. Ogni categoria indica caratteristiche e scopi differenti per i certificati. Ad esempio My sono di norma i certificati autoprodotti e che hanno validità solo all'interno della macchina o per l'utente (a seconda dello StoreLocation).
  • SubjectName:
    È la stringa che identifica l'intestatario del certificato è che è contenuta nel parametro chiamato Common Name del certificato stesso.

La classe Common è una semplice definizione di alcune stringhe utili per il recupero dei parametri dalla sezione AppSettings del Web.config.

public const string IssuerName = "IssuerName";
public const string SigningCertificateName = "SigningCertificateName";
public const string EncryptingCertificateName = "EncryptingCertificateName";

 

Il codice prodotto dal wizard è, per ovvi motivi, piuttosto semplificato e prevede che l'STS serva un solo RP. Chiaramente in un caso reale le informazioni  andranno memorizzate tramite una struttura più complessa, ma non è difficile immaginare l'utilizzo di un elenco presente in un file di configurazione o su un database.

Torniamo al punto in cui viene chiamato in causa il STS. L'esempio estende due classi di WIF. La classe CustomSecurityTokenService estende la classe che fornisce le funzionalità del STS, mente la classe CustomSecurityTokenServiceConfiguration estende la classe che contiene i parametri di configurazione del STS.

La classe CustomSecurityTokenServiceConfiguration, oltre a definire la proprietà Current come singleton della classe stessa, perché sia riutilizzabile più volte, devinisce anche un costruttore che ha lo scopo di inizializzare l'istanza, con i parametri presenti in AppSettings del Web.config. Questi parametri servono alla classe per recuperare il certificato che verrà utilizzato del STS per firmare i token.

Tale istanza viene utilizzata nel code-behind della pagina Default.aspx, che abbiamo visto in precedenza, come parametro di configurazione per la creazione della classe CustomSecurityTokenService. L'istanza viene quindi utilizzata dal metodo ProcessSignInRequest  nel  codice visto in precedenza, che riporto per comodità:

SecurityTokenService sts = new CustomSecurityTokenService( CustomSecurityTokenServiceConfiguration.Current );
SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest( requestMessage, User, sts );
FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse( responseMessage, Response );

 

Cosa succede durante la chiamata al metodo ProcessSignInRequest? Il metodo ProcessSignInRequest richiama in sequenza i segenti metodi della classe CustomSecurityTokenService:

  • GetScope
  • GetOutputClaimsIdentity

I metodi in questione sono ridefiniti. Il metodo GetScope, riceve in input il contratto che rappresenta l'utente che si è autenticato presso l'IP ed una istanza dell'oggetto che rappresenta la richiesta di SignIn. Il metodo ha il compito di tornate un'istanza configurata della classe Microsoft.IdentityModel.SecurityTokenService.Scope. La classe in questione contiene alcune informazioni che serviranno in seguito per procedere ad una eventuale crittografia del SecureToken. Il Security Token come sappiamo contiene informazioni di qualsiasi tipo e quindi è probabile, anche se non sempre, che si voglia che tali dati siano visibili soltanto al RP.

Il cuore del STS è il metodo GetOutputClaimsIdentity, dove vengono aggiunte finalmente le claim che verranno firmate ed eventualmente criptate nel Security Token. Le claim vengono agginte all'omonima collezione dell'istanza della classe ClaimsIdentity, che viene tornata dalla funzione. ClaimsIdentity è implementa IClaimsIdentity che a sua volta è un'estensione  dell'interfaccia IIdentity. IIdentity è l'interfaccia di base che rappresenta l'identità dell'utente corrente associato ad una richiesta http. Questo vuol dire, che nell'applicazione web del RP l'istanza rappresentata da HttpContext.Current.User.Identity è un'istanza di tipo ClaimsIdentity che implementa IClaimsIdentity.

Ovviamente eseguiremo un cast poiché sappaimo che nel contesto in cui siamo l'istanza reale che è associata all'identità è di tipo IClaimsIdentity.

Vediamo come è composta questa interfaccia e come possiamo sfruttare le informazioni presenti in essa.

public interface IClaimsIdentity : IIdentity
{
    IClaimsIdentity Actor { get; set; }
    SecurityToken BootstrapToken { get; set; }
    ClaimCollection Claims { get; }
    string Label { get; set; }
    string NameClaimType { get; set; }
    string RoleClaimType { get; set; }
 
    IClaimsIdentity Copy();
}

 

La proprietà Actor contiene l'identità dell'utente a cui appartiene il set di claim, mentre le proprietà NameClaimType e RoleClaimType contengono gli identificativi dei tipi di claim che contengono rispettivamente l'identificativo dell'utente ed i ruoli dell'utente. Ovviamente per il secondo potremo avere più claim con il tipo relativo al ruolo.

Queste due stringhe vengono utilizzate dallo strato di autenticazione di ASP per mappare queste informazioni nelle classiche strutture. In questo modo sarà possibile identificare l'utente associato alla richiesta http ed i suoi ruoli nello stesso identico modo degli altri tipi di autenticazione. Potremo infatti utilizzare il consueto metodo HttpContext.Current.User.IsInRole("ruolo") per sapere se un utente è associato o meno ad un certo ruolo. Una conseguenza, importante, di questo è che l'utilizzo dello scenario di autenticazione federata, non ci costringe a rivedere l'architettura applicativa esistente.

Rispetto ai classici metodi di autenticazione nativi presenti in ASP.Net, l'autenticazione basata su claim mette a disposizione, una serie di informazioni maggiore. Nella collection Claims di IClaimsIdentity, infatti, possiamo avere un insieme arbitrario di attributi associati all'utente, come ad esempio l'indirizzo email, la data di nascita, o qualsiasi altro attributo messo a disposizione del STS dal IP. Una delle conseguenze possibili è, ad esempio, la possibilità di non dover gestire alcuna profilazione dell'utente sulla nostra applicazione, poiché questi dati possono ci vengono forniti dall'esterno tramite le claim.

È possibile accedere alla collection di claim per indice oppure è possibile utilizzare linq per verificare la presenza di una claim o estrarne il valore. La classe Claim contiene la proprietà ClaimType di tipo string. Questa proprietà rappresenta un URI che definisce univocamente un tipo di claim. Si può trattare di un URI arbitrario che potrebbe anche avere senso solamente all'interno del singolo RP. Potrebbe, per esempio, identificare un particolare permesso applicativo dell'utente.

Anche se l'URI che rappresenta una claim può essere arbitrariamente scelta è molto utile, invece, potersi avvalere di identificativi universalmente riconosciuti. Alcuni di questi sono definiti come standard all'interno delle specifiche SAML (Security Assertion Markup Language) che è il formato basato su xml con il quale vengono rappresentate le claim. Alcuni di questi identificativi sono contenuti come costanti nella classe Microsoft.IdentityModel.Claims.ClaimTypes.

 
public const string Email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
 

 

Conclusioni

Nel prossimo articolo vedremo alcuni aspetti legati alla sicurezza. Sposteremo il nostro servizio su IIS per utilizzare il protocollo https e vedremo come creare i certificati digitali da utilizzare per la cifratura e la firma dei Security Token.


Tags: WIF,Windows Identity Foundation

 
x