Windows Identity Framework – Il Secure Token Service
Scritto da
Luca Cestola
il
martedì 10 maggio 2011
Linguaggio:
•
Framework:
•
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".

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.

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