Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 (Proteggere un'API Web con account singoli e account di accesso locale in API Web ASP.NET 2.2)
di Mike Wasson
Questo argomento illustra come proteggere un'API Web usando OAuth2 per l'autenticazione in un database di appartenenza.
Versioni software usate nell'esercitazione
In Visual Studio 2013 il modello di progetto API Web offre tre opzioni per l'autenticazione:
- Singoli account. L'app usa un database di appartenenza.
- Account dell'organizzazione. Gli utenti accedono con le credenziali di Azure Active Directory, Office 365 o Active Directory locale.
- Autenticazione Windows. Questa opzione è destinata alle applicazioni Intranet e usa il modulo IIS per l'autenticazione di Windows.
I singoli account consentono a un utente di accedere in due modi:
- Account di accesso locale. L'utente si registra nel sito, immettendo un nome utente e una password. L'app archivia l'hash delle password nel database di appartenenza. Quando l'utente accede, il sistema di identità ASP.NET verifica la password.
- Account di accesso social. L'utente accede con un servizio esterno, ad esempio Facebook, Microsoft o Google. L'app crea ancora una voce per l'utente nel database di appartenenza, ma non archivia credenziali. L'utente esegue l'autenticazione accedendo al servizio esterno.
Questo articolo illustra lo scenario di accesso locale. Per l'account di accesso locale e di social networking, l'API Web usa OAuth2 per autenticare le richieste. Tuttavia, i flussi di credenziali sono diversi per l'account di accesso locale e di social networking.
In questo articolo verrà illustrata una semplice app che consente all'utente di accedere e inviare chiamate AJAX autenticate a un'API Web. È possibile scaricare il codice di esempio qui. Il file leggimi descrive come creare l'esempio da zero in Visual Studio.
L'app di esempio usa Knockout.js per l'associazione dati e jQuery per l'invio di richieste AJAX. Mi concentrerò sulle chiamate AJAX, quindi non è necessario conoscere Knockout.js per questo articolo.
Lungo la strada, descriverei:
- Operazioni eseguite dall'app sul lato client.
- Cosa succede sul server.
- Traffico HTTP al centro.
Prima di tutto, è necessario definire una terminologia OAuth2.
- Risorsa. Alcuni dati che possono essere protetti.
- Server di risorse. Server che ospita la risorsa.
- Proprietario della risorsa. Entità che può concedere l'autorizzazione per accedere a una risorsa. (In genere l'utente.
- Client: l'app che vuole accedere alla risorsa. In questo articolo il client è un Web browser.
- Token di accesso. Token che concede l'accesso a una risorsa.
- Token di connessione. Un particolare tipo di token di accesso, con la proprietà che chiunque può usare il token. In altre parole, un client non necessita di una chiave crittografica o di un altro segreto per usare un token di connessione. Per questo motivo, i token di connessione devono essere usati solo su HTTPS e devono avere tempi di scadenza relativamente brevi.
- Server di autorizzazione. Server che fornisce token di accesso.
Un'applicazione può fungere sia da server di autorizzazione che da server di risorse. Il modello di progetto API Web segue questo modello.
Flusso di credenziali di accesso locale
Per l'accesso locale, l'API Web usa il flusso di password del proprietario della risorsa definito in OAuth2.
- L'utente immette un nome e una password nel client.
- Il client invia queste credenziali al server di autorizzazione.
- Il server di autorizzazione autentica le credenziali e restituisce un token di accesso.
- Per accedere a una risorsa protetta, il client include il token di accesso nell'intestazione Authorization della richiesta HTTP.
Quando si seleziona Account singoli nel modello di progetto API Web, il progetto include un server di autorizzazione che convalida le credenziali utente e rilascia i token. Il diagramma seguente illustra lo stesso flusso di credenziali in termini di componenti dell'API Web.
In questo scenario, i controller API Web fungono da server di risorse. Un filtro di autenticazione convalida i token di accesso e l'attributo [Authorize] viene usato per proteggere una risorsa. Quando un controller o un'azione ha l'attributo [Authorize] , tutte le richieste a tale controller o azione devono essere autenticate. In caso contrario, l'autorizzazione viene negata e l'API Web restituisce un errore 401 (non autorizzato).
Il server di autorizzazione e il filtro di autenticazione chiamano entrambi in un componente middleware OWIN che gestisce i dettagli di OAuth2. La progettazione verrà descritta in modo più dettagliato più avanti in questa esercitazione.
Invio di una richiesta non autorizzata
Per iniziare, eseguire l'app e fare clic sul pulsante Chiama API . Al termine della richiesta, nella casella Risultato verrà visualizzato un messaggio di errore. Ciò è dovuto al fatto che la richiesta non contiene un token di accesso, pertanto la richiesta non è autorizzata.
Il pulsante Chiama API invia una richiesta AJAX a ~/api/values, che richiama un'azione del controller API Web. Ecco la sezione del codice JavaScript che invia la richiesta AJAX. Nell'app di esempio tutto il codice dell'app JavaScript si trova nel file Scripts\app.js.
// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
headers.Authorization = 'Bearer ' + token;
}
$.ajax({
type: 'GET',
url: 'api/values/1',
headers: headers
}).done(function (data) {
self.result(data);
}).fail(showError);
Fino a quando l'utente non accede, non è presente alcun token di connessione e pertanto non è presente alcuna intestazione di autorizzazione nella richiesta. In questo modo la richiesta restituisce un errore 401.
Ecco la richiesta HTTP. (Ho usato Fiddler per acquisire il traffico HTTP.
GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Risposta HTTP:
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61
{"Message":"Authorization has been denied for this request."}
Si noti che la risposta include un'intestazione Www-Authenticate con la richiesta di verifica impostata su Bearer. Ciò indica che il server prevede un token di connessione.
Registrare un utente
Nella sezione Registra dell'app immettere un messaggio di posta elettronica e una password e fare clic sul pulsante Registra.
Non è necessario usare un indirizzo di posta elettronica valido per questo esempio, ma un'app reale conferma l'indirizzo. (Vedere Creare un'app Web di ASP.NET MVC 5 sicura con accesso, conferma tramite posta elettronica e reimpostazione della password. Per la password, usare un carattere simile a "Password1!", con una lettera maiuscola, una lettera minuscola, un numero e un carattere non alfanumerico. Per mantenere l'app semplice, la convalida lato client è stata interrotta, quindi se si verifica un problema con il formato della password, si riceverà un errore 400 (richiesta non valida).
Il pulsante Registra invia una richiesta POST a ~/api/Account/Register/. Il corpo della richiesta è un oggetto JSON che contiene il nome e la password. Ecco il codice JavaScript che invia la richiesta:
var data = {
Email: self.registerEmail(),
Password: self.registerPassword(),
ConfirmPassword: self.registerPassword2()
};
$.ajax({
type: 'POST',
url: '/api/Account/Register',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function (data) {
self.result("Done!");
}).fail(showError);
Richiesta HTTP, dove $CREDENTIAL_PLACEHOLDER$
è un segnaposto per la coppia chiave-valore della password:
POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84
{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}
Risposta HTTP:
HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0
Questa richiesta viene gestita dalla AccountController
classe . Internamente, AccountController
usa ASP.NET Identity per gestire il database di appartenenza.
Se si esegue l'app in locale da Visual Studio, gli account utente vengono archiviati in LocalDB nella tabella AspNetUsers. Per visualizzare le tabelle in Visual Studio, fare clic sul menu Visualizza , selezionare Esplora server e quindi espandere Connessioni dati.
Ottenere un token di accesso
Finora non è stato eseguito alcun OAuth, ma ora verrà visualizzato il server di autorizzazione OAuth in azione, quando si richiede un token di accesso. Nell'area Accesso dell'app di esempio immettere il messaggio di posta elettronica e la password e fare clic su Accedi.
Il pulsante Accedi invia una richiesta all'endpoint del token. Il corpo della richiesta contiene i dati con codifica form-url:The body of the request contains the following form-url-encoded data:
- grant_type: "password"
- username: <indirizzo di posta elettronica dell'utente>
- password: <password>
Ecco il codice JavaScript che invia la richiesta AJAX:
var loginData = {
grant_type: 'password',
username: self.loginEmail(),
password: self.loginPassword()
};
$.ajax({
type: 'POST',
url: '/Token',
data: loginData
}).done(function (data) {
self.user(data.userName);
// Cache the access token in session storage.
sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);
Se la richiesta ha esito positivo, il server di autorizzazione restituisce un token di accesso nel corpo della risposta. Si noti che il token viene archiviato nell'archivio sessioni, da usare in un secondo momento quando si inviano richieste all'API. A differenza di alcune forme di autenticazione (ad esempio l'autenticazione basata su cookie), il browser non includerà automaticamente il token di accesso nelle richieste successive. L'applicazione deve farlo in modo esplicito. Questa è una buona cosa, perché limita le vulnerabilità CSRF.
Richiesta HTTP:
POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68
grant_type=password&username=alice%40example.com&password=Password1!
È possibile notare che la richiesta contiene le credenziali dell'utente. È necessario usare HTTPS per garantire la sicurezza del livello di trasporto.
Risposta HTTP:
HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT
{
"access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
"token_type":"bearer",
"expires_in":1209599,
"userName":"alice@example.com",
".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}
Per la leggibilità, ho rientrato il codice JSON e troncato il token di accesso, che è molto lungo.
Le access_token
proprietà , token_type
e expires_in
sono definite dalla specifica OAuth2. Le altre proprietà (userName
, .issued
e .expires
) sono solo a scopo informativo. È possibile trovare il codice che aggiunge tali proprietà aggiuntive nel TokenEndpoint
metodo , nel file /Providers/ApplicationOAuthProvider.cs.
Inviare una richiesta autenticata
Ora che è disponibile un token di connessione, è possibile effettuare una richiesta autenticata all'API. Questa operazione viene eseguita impostando l'intestazione Authorization nella richiesta. Fare di nuovo clic sul pulsante Chiama API per visualizzarla.
Richiesta HTTP:
GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest
Risposta HTTP:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27
"Hello, alice@example.com."
Disconnetti
Poiché il browser non memorizza nella cache le credenziali o il token di accesso, la disconnessione è semplicemente una questione di "dimenticanza" del token, rimuovendola dall'archiviazione sessione:
self.logout = function () {
sessionStorage.removeItem(tokenKey)
}
Informazioni sul modello di progetto Account singoli
Quando si seleziona Account singoli nel modello di progetto applicazione Web ASP.NET, il progetto include:
- Un server di autorizzazione OAuth2.
- Un endpoint API Web per la gestione degli account utente
- Modello di Entity Framework per l'archiviazione degli account utente.
Ecco le classi di applicazione principali che implementano queste funzionalità:
AccountController
. Fornisce un endpoint API Web per la gestione degli account utente. L'azioneRegister
è l'unica usata in questa esercitazione. Altri metodi della classe supportano la reimpostazione della password, gli account di accesso di social networking e altre funzionalità.ApplicationUser
, definito in /Models/IdentityModels.cs. Questa classe è il modello ef per gli account utente nel database di appartenenza.ApplicationUserManager
, definito in /App_Start/IdentityConfig.cs Questa classe deriva da UserManager ed esegue operazioni sugli account utente, ad esempio la creazione di un nuovo utente, la verifica delle password e così via e mantiene automaticamente le modifiche al database.ApplicationOAuthProvider
. Questo oggetto si collega al middleware OWIN ed elabora gli eventi generati dal middleware. Deriva da OAuthAuthorizationServerProvider.
Configurazione del server di autorizzazione
In StartupAuth.cs il codice seguente configura il server di autorizzazione OAuth2.
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// Note: Remove the following line before you deploy to production:
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
La TokenEndpointPath
proprietà è il percorso URL dell'endpoint del server di autorizzazione. Si tratta dell'URL usato dall'app per ottenere i token di connessione.
La Provider
proprietà specifica un provider che collega il middleware OWIN ed elabora gli eventi generati dal middleware.
Ecco il flusso di base quando l'app vuole ottenere un token:
- Per ottenere un token di accesso, l'app invia una richiesta a ~/Token.
- Il middleware OAuth chiama
GrantResourceOwnerCredentials
il provider. - Il provider chiama per
ApplicationUserManager
convalidare le credenziali e creare un'identità delle attestazioni. - Se l'operazione ha esito positivo, il provider crea un ticket di autenticazione, che viene usato per generare il token.
Il middleware OAuth non conosce nulla sugli account utente. Il provider comunica tra il middleware e ASP.NET Identity. Per altre informazioni sull'implementazione del server di autorizzazione, vedere Server di autorizzazione OWIN OAuth 2.0.
Configurazione dell'API Web per l'uso dei token di connessione
WebApiConfig.Register
Nel metodo il codice seguente configura l'autenticazione per la pipeline dell'API Web:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
La classe HostAuthenticationFilter abilita l'autenticazione usando token di connessione.
Il metodo SuppressDefaultHostAuthentication indica all'API Web di ignorare qualsiasi autenticazione eseguita prima che la richiesta raggiunga la pipeline dell'API Web, da IIS o dal middleware OWIN. In questo modo, è possibile limitare l'autenticazione dell'API Web usando solo i token di connessione.
Nota
In particolare, la parte MVC dell'app potrebbe usare l'autenticazione basata su form, che archivia le credenziali in un cookie. L'autenticazione basata su cookie richiede l'uso di token anti-falsi, per evitare attacchi CSRF. Questo è un problema per le API Web, perché non esiste un modo pratico per l'API Web di inviare il token anti-falsità al client. Per altre informazioni su questo problema, vedere Prevenzione degli attacchi CSRF nell'API Web. La chiamata a SuppressDefaultHostAuthentication garantisce che l'API Web non sia vulnerabile agli attacchi CSRF dalle credenziali archiviate nei cookie.
Quando il client richiede una risorsa protetta, ecco cosa accade nella pipeline dell'API Web:
- Il filtro HostAuthentication chiama il middleware OAuth per convalidare il token.
- Il middleware converte il token in un'identità di attestazioni.
- A questo punto, la richiesta viene autenticata ma non autorizzata.
- Il filtro di autorizzazione esamina l'identità delle attestazioni. Se le attestazioni autorizzano l'utente per tale risorsa, la richiesta è autorizzata. Per impostazione predefinita, l'attributo [Authorize] autorizza tutte le richieste autenticate. Tuttavia, è possibile autorizzare in base al ruolo o ad altre attestazioni. Per altre informazioni, vedere Autenticazione e autorizzazione nell'API Web.
- Se i passaggi precedenti hanno esito positivo, il controller restituisce la risorsa protetta. In caso contrario, il client riceve un errore 401 (non autorizzato).
Risorse aggiuntive
- identità ASP.NET
- Informazioni sulle funzionalità di sicurezza nel modello SPA per VS2013 RC. Post di blog MSDN di Hongye Sun.
- Definizione del modello di account individuali dell'API Web- Parte 2: Account locali. Post di blog di Dominick Baier.
- Autenticazione host e API Web con OWIN. Una buona spiegazione di
SuppressDefaultHostAuthentication
eHostAuthenticationFilter
di Brock Allen. - Personalizzazione delle informazioni sul profilo nei modelli di ASP.NET Identity nei modelli di Visual Studio 2013. Post di blog MSDN di Pranav Rastogi.
- Gestione della durata delle richieste per la classe UserManager in ASP.NET Identity. Post di blog MSDN di Suhas Joshi, con una buona spiegazione della
UserManager
classe.