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

Scaricare l'app di esempio

Questo argomento illustra come proteggere un'API Web usando OAuth2 per eseguire l'autenticazione su 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 aziendali. Gli utenti accedono con Azure Active Directory, Office 365 o credenziali di Active Directory locali.
  • Autenticazione Windows. Questa opzione è destinata alle applicazioni Intranet e usa il modulo IIS di autenticazione di Windows.

Per altre informazioni su queste opzioni, vedere Creazione di progetti Web ASP.NET in Visual Studio 2013.

I singoli account offrono due modi per accedere a un utente:

  • Account di accesso locale. L'utente 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 ASP.NET Identity 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 alcuna 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 social, l'API Web usa OAuth2 per autenticare le richieste. Tuttavia, i flussi delle credenziali sono diversi per l'account di accesso locale e social.

In questo articolo viene 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 readme descrive come creare l'esempio da zero in Visual Studio.

Immagine del modulo di esempio

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, descriverò:

  • Cosa sta facendo l'app sul lato client.
  • Cosa accade nel server.
  • Il traffico HTTP al centro.

Prima di tutto, è necessario definire una terminologia OAuth2.

  • Risorsa. Alcuni dati che possono essere protetti.
  • Server 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 ha bisogno 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 i 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 proprietario della risorsa definito in OAuth2.

  1. L'utente immette un nome e una password nel client.
  2. Il client invia queste credenziali al server di autorizzazione.
  3. Il server di autorizzazione autentica le credenziali e restituisce un token di accesso.
  4. Per accedere a una risorsa protetta, il client include il token di accesso nell'intestazione Di autorizzazione della richiesta HTTP.

Diagramma del flusso delle credenziali di accesso locale

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 api Web.

Diagramma quando vengono selezionati singoli account nel Web A I

In questo scenario, i controller API Web fungono da server di risorse. Un filtro di autenticazione convalida i token di accesso e l'attributo [Autorizza] viene usato per proteggere una risorsa. Quando un controller o un'azione ha l'attributo [Autorizza] , 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 si chiamano entrambi in un componente middleware OWIN che gestisce i dettagli di OAuth2. Descriverò più in dettaglio la progettazione 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, verrà visualizzato un messaggio di errore nella casella Risultato . Questo perché la richiesta non contiene un token di accesso, quindi la richiesta non è autorizzata.

Immagine del messaggio di errore del risultato

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 quindi nessuna 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 sfida 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 con accesso, conferma della posta elettronica e reimpostazione della password. Per la password, usare un valore simile a "Password1!", con una lettera maiuscola, una lettera minuscola, un numero e un carattere non alfa-numerico. Per mantenere l'app semplice, ho lasciato la convalida lato client, quindi se si verifica un problema con il formato password, si otterrà un errore 400 (richiesta non valida).

Immagine della registrazione di una sezione utente

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

Immagine delle connessioni dati

Ottenere un token di accesso

Finora non è stato fatto 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 la posta elettronica e la password e fare clic su Accedi.

Immagine della sezione Log in

Il pulsante Accedi invia una richiesta all'endpoint del token. Il corpo della richiesta contiene i dati con codifica url del modulo seguenti:

  • grant_type: "password"
  • username: <il messaggio 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'archiviazione sessione, da usare in un secondo momento durante l'invio di 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 fornire 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 rientro il codice JSON e troncato il token di accesso, che è molto lungo.

Le access_tokenproprietà , token_typee 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 metodo , nel TokenEndpoint file /Providers/ApplicationOAuthProvider.cs.

Inviare una richiesta autenticata

Ora che è disponibile un token di connessione, è possibile eseguire una richiesta autenticata all'API. Questa operazione viene eseguita impostando l'intestazione Di autorizzazione nella richiesta. Fare di nuovo clic sul pulsante Chiama API per visualizzarla.

Immagine dopo aver chiamato un pulsante I P è stato fatto clic su

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

Log Out

Poiché il browser non memorizza nella cache le credenziali o il token di accesso, la disconnessione è semplicemente una questione di "dimenticanza" del token, rimuovendolo 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 ASP.NET applicazione Web, il progetto include:

  • Server di autorizzazione OAuth2.
  • Endpoint API Web per la gestione degli account utente
  • Modello EF per l'archiviazione degli account utente.

Ecco le classi dell'applicazione principali che implementano queste funzionalità:

  • AccountController. Fornisce un endpoint API Web per la gestione degli account utente. L'azione Register è l'unica usata in questa esercitazione. Altri metodi nella classe supportano la reimpostazione della password, gli account di accesso social 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 così via e mantiene automaticamente le modifiche al database.
  • ApplicationOAuthProvider. Questo oggetto si collega al middleware OWIN e elabora gli eventi generati dal middleware. Deriva da OAuthAuthorizationServerProvider.

Immagine delle classi di applicazioni principali

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. Questo è l'URL usato dall'app per ottenere i token di connessione.

La Provider proprietà specifica un provider che si collega al middleware OWIN e elabora gli eventi generati dal middleware.

Ecco il flusso di base quando l'app vuole ottenere un token:

  1. Per ottenere un token di accesso, l'app invia una richiesta a ~/Token.
  2. Il middleware OAuth chiama GrantResourceOwnerCredentials il provider.
  3. Il provider chiama l'oggetto per convalidare le credenziali e creare un'identità ApplicationUserManager delle attestazioni.
  4. In caso di esito positivo, il provider crea un ticket di autenticazione che viene usato per generare il token.

Diagramma del flusso di autorizzazione

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 OWIN OAuth 2.0 Authorization Server.

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 i token di connessione.

Il metodo SuppressDefaultHostAuthentication indica all'API Web di ignorare qualsiasi autenticazione che si verifica 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 dei moduli, che archivia le credenziali in un cookie. L'autenticazione basata su cookie richiede l'uso di token anti-forgery, per evitare attacchi CSRF. Questo è un problema per le API Web, perché non esiste alcun modo pratico per l'API Web per inviare il token anti-forgery 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:

  1. Il filtro HostAuthentication chiama il middleware OAuth per convalidare il token.
  2. Il middleware converte il token in un'identità delle attestazioni.
  3. A questo punto, la richiesta viene autenticata ma non autorizzata.
  4. 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 [Autorizza] autorizza qualsiasi richiesta autenticata. Tuttavia, è possibile autorizzare per ruolo o per altre attestazioni. Per altre informazioni, vedere Autenticazione e autorizzazione nell'API Web.
  5. Se i passaggi precedenti hanno esito positivo, il controller restituisce la risorsa protetta. In caso contrario, il client riceve un errore 401 (non autorizzato).

Diagramma di quando il client richiede una risorsa protetta

Risorse aggiuntive