Een web-API beveiligen met afzonderlijke accounts en lokale aanmelding in ASP.NET Web-API 2.2
door Mike Wasson
In dit onderwerp wordt beschreven hoe u een web-API beveiligt met behulp van OAuth2 voor verificatie bij een lidmaatschapsdatabase.
Softwareversies die in de zelfstudie worden gebruikt
In Visual Studio 2013 biedt de web-API-projectsjabloon drie opties voor verificatie:
- Afzonderlijke accounts. De app maakt gebruik van een lidmaatschapsdatabase.
- Organisatieaccounts. Gebruikers melden zich aan met hun Azure Active Directory-, Office 365- of on-premises Active Directory-referenties.
- Windows-verificatie. Deze optie is bedoeld voor intranettoepassingen en maakt gebruik van de IIS-module Windows-verificatie.
Afzonderlijke accounts bieden twee manieren waarop een gebruiker zich kan aanmelden:
- Lokale aanmelding. De gebruiker registreert zich op de site en voert een gebruikersnaam en wachtwoord in. De app slaat de wachtwoord-hash op in de lidmaatschapsdatabase. Wanneer de gebruiker zich aanmeldt, controleert het ASP.NET Identiteitssysteem het wachtwoord.
- Social login. De gebruiker meldt zich aan met een externe service, zoals Facebook, Microsoft of Google. De app maakt nog steeds een vermelding voor de gebruiker in de lidmaatschapsdatabase, maar slaat geen referenties op. De gebruiker wordt geverifieerd door u aan te melden bij de externe service.
In dit artikel wordt het lokale aanmeldingsscenario beschreven. Voor zowel lokale als sociale aanmelding gebruikt web-API OAuth2 om aanvragen te verifiëren. De inloggegevensstromen verschillen echter voor lokaal en sociaal inloggen.
In dit artikel laat ik een eenvoudige app zien waarmee de gebruiker zich kan aanmelden en geverifieerde AJAX-aanroepen naar een web-API kan verzenden. U kunt de voorbeeldcode hier downloaden. In het leesmij-bestand wordt beschreven hoe u het voorbeeld vanaf nul maakt in Visual Studio.
De voorbeeld-app maakt gebruik van Knockout.js voor gegevensbinding en jQuery voor het verzenden van AJAX-aanvragen. Ik richt me op de AJAX-oproepen, dus je hoeft niet te weten Knockout.js voor dit artikel.
Onderweg zal ik het volgende beschrijven:
- Wat de app aan de clientzijde doet.
- Wat gebeurt er op de server.
- Het HTTP-verkeer in het midden.
Eerst moeten we een aantal OAuth2-terminologie definiëren.
- Resource. Een deel van de gegevens die kunnen worden beveiligd.
- resourceserver. De server die als host fungeert voor de resource.
- De resource-eigenaar. De entiteit die machtigingen kan verlenen voor toegang tot een resource. (Meestal de gebruiker.)
- Client: de app die toegang tot de resource wil. In dit artikel is de client een webbrowser.
- Access-token. Een token dat toegang verleent tot een resource.
- Bearer-token. Een bepaald type toegangstoken, met de eigenschap die iedereen het token kan gebruiken. Met andere woorden, een client heeft geen cryptografische sleutel of ander geheim nodig om een bearer-token te gebruiken. Daarom mogen bearer-tokens alleen worden gebruikt via een HTTPS en moeten ze relatief korte verlooptijden hebben.
- Autorisatieserver. Een server die toegangstokens verleent.
Een toepassing kan fungeren als zowel autorisatieserver als resourceserver. De web-API-projectsjabloon volgt dit patroon.
Lokale inloggegevensstroom
Voor lokale aanmelding maakt web-API gebruik van de wachtwoordstroom voor resource-eigenaar gedefinieerd in OAuth2.
- De gebruiker voert een naam en wachtwoord in de client in.
- De client verzendt deze referenties naar de autorisatieserver.
- De autorisatieserver verifieert de referenties en retourneert een toegangstoken.
- Voor toegang tot een beveiligde resource bevat de client het toegangstoken in de autorisatieheader van de HTTP-aanvraag.
Wanneer u individuele accounts selecteert in het web-API-projectsjabloon, bevat het project een autorisatieserver die gebruikersreferenties valideert en tokens uitgeeft. In het volgende diagram ziet u dezelfde referentiestroom in termen van web-API-onderdelen.
In dit scenario fungeren web-API-controllers als resourceservers. Een verificatiefilter valideert toegangstokens en het [Autoriseren] kenmerk wordt gebruikt om een resource te beveiligen. Wanneer een controller of actie het kenmerk [Autoriseren] heeft, moeten alle aanvragen voor die controller of actie worden geverifieerd. Anders wordt autorisatie geweigerd en retourneert de Web-API een fout 401 (Niet geautoriseerd).
De autorisatieserver en het verificatiefilter roepen beide een OWIN-middleware component aan, dat de details van OAuth2 verwerkt. Verderop in deze zelfstudie zal ik het ontwerp in meer detail beschrijven.
Een niet-geautoriseerde aanvraag verzenden
Voer de app uit om aan de slag te gaan en klik op de knop API aanroepen. Wanneer de aanvraag is voltooid, ziet u een foutbericht in het vak Resultaat. Dat komt doordat de aanvraag geen toegangstoken bevat, dus de aanvraag is niet gemachtigd.
De knop API aanroepen verzendt een AJAX-aanvraag naar ~/api/waarden, waarmee een web-API-controlleractie wordt aangeroepen. Hier volgt de sectie van JavaScript-code waarmee de AJAX-aanvraag wordt verzonden. In de voorbeeld-app bevindt alle JavaScript-app-code zich in het bestand 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);
Totdat de gebruiker zich aanmeldt, is er geen bearer-token en daarom geen autorisatieheader in de aanvraag. Dit zorgt ervoor dat de aanvraag een 401-fout retourneert.
Dit is de HTTP-aanvraag. (Ik heb Fiddler gebruikt om het HTTP-verkeer vast te leggen.)
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/
HTTP-antwoord:
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."}
Let op dat het antwoord een Www-Authenticate-header bevat met de uitdaging ingesteld op "Bearer". Dit geeft aan dat de server een Bearer-token verwacht.
Een gebruiker registreren
Voer in de sectie van de app registreren een e-mailadres en wachtwoord in en klik op de knop registreren.
U hoeft geen geldig e-mailadres voor dit voorbeeld te gebruiken, maar een echte app bevestigt het adres. (Zie Een beveiligde ASP.NET MVC 5-web-app maken met aanmelden, e-mailbevestiging en wachtwoordherstel.) Gebruik voor het wachtwoord iets als 'Wachtwoord1!', met een hoofdletter, kleine letter, cijfer en niet-alfanumerieke tekens. Om de app eenvoudig te houden, heb ik validatie aan de clientzijde weggelaten. Als er dus een probleem is met de wachtwoordindeling, krijgt u een 400-fout (Ongeldige aanvraag).
De knop Registreren verzendt een POST-aanvraag naar ~/api/Account/Register/. De aanvraagtekst is een JSON-object dat de naam en het wachtwoord bevat. Dit is de JavaScript-code waarmee de aanvraag wordt verzonden:
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);
HTTP-aanvraag, waarbij $CREDENTIAL_PLACEHOLDER$
een tijdelijke aanduiding is voor het wachtwoordsleutel-waardepaar:
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$"}
HTTP-antwoord:
HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0
Deze aanvraag wordt verwerkt door de klasse AccountController
. Intern gebruikt AccountController
ASP.NET Identiteit om de lidmaatschapsdatabase te beheren.
Als u de app lokaal uitvoert vanuit Visual Studio, worden gebruikersaccounts opgeslagen in LocalDB, in de tabel AspNetUsers. Als u de tabellen in Visual Studio wilt weergeven, klikt u op het menu Weergave, selecteert u Server Explorer-en vouwt u Gegevensverbindingen uit.
Een toegangstoken ophalen
Tot nu toe hebben we geen OAuth uitgevoerd, maar nu zien we de OAuth-autorisatieserver in actie, wanneer we een toegangstoken aanvragen. Voer in het gebied Aanmelden van de voorbeeld-app het e-mailadres en wachtwoord in en klik op Aanmelden.
De knop Aanmelden verzendt een aanvraag naar het tokeneindpunt. De hoofdtekst van de aanvraag bevat de volgende formulier-URL-gecodeerde gegevens:
- grant_type: 'wachtwoord'
- gebruikersnaam: <het e-mailadres van de gebruiker>
- wachtwoord: <wachtwoord>
Dit is de JavaScript-code waarmee de AJAX-aanvraag wordt verzonden:
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);
Als de aanvraag slaagt, retourneert de autorisatieserver een toegangstoken in de hoofdtekst van het antwoord. U ziet dat we het token opslaan in sessieopslag om later te gebruiken bij het verzenden van aanvragen naar de API. In tegenstelling tot sommige vormen van verificatie (zoals verificatie op basis van cookies), wordt het toegangstoken niet automatisch opgenomen in volgende aanvragen. De toepassing moet dit expliciet doen. Dat is een goede zaak, omdat het CSRF-beveiligingsproblemenbeperkt.
HTTP-aanvraag:
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!
U kunt zien dat de aanvraag de referenties van de gebruiker bevat. U moet HTTPS gebruiken om transportlaagbeveiliging te bieden.
HTTP-antwoord:
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"
}
Voor de leesbaarheid heb ik de JSON ingesprongen en het toegangstoken, dat vrij lang is, afgekapt.
De eigenschappen access_token
, token_type
en expires_in
worden gedefinieerd door de OAuth2-specificatie. De andere eigenschappen (userName
, .issued
en .expires
) zijn slechts ter informatie bedoeld. U vindt de code waarmee deze aanvullende eigenschappen worden toegevoegd in de methode TokenEndpoint
, in het bestand /Providers/ApplicationOAuthProvider.cs.
Een geverifieerde aanvraag verzenden
Nu we een Bearer-token hebben, kunnen we een geverifieerde aanvraag indienen bij de API. Dit wordt gedaan door de autorisatieheader in de aanvraag in te stellen. Klik nogmaals op de knop Call API om dit te zien.
HTTP-aanvraag:
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
HTTP-antwoord:
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."
Uitloggen
Omdat de browser de referenties of het toegangstoken niet in de cache opbergt, is afmelden gewoon een kwestie van het 'vergeten' van het token door het te verwijderen uit sessieopslag:
self.logout = function () {
sessionStorage.removeItem(tokenKey)
}
Het begrijpen van de projectsjabloon voor afzonderlijke accounts
Wanneer u Individual Accounts selecteert in de ASP.NET-webtoepassing projectsjabloon, bevat het project:
- Een OAuth2-autorisatieserver.
- Een web-API-eindpunt voor het beheren van gebruikersaccounts
- Een EF-model voor het opslaan van gebruikersaccounts.
Dit zijn de belangrijkste toepassingsklassen die deze functies implementeren:
-
AccountController
. Biedt een web-API-eindpunt voor het beheren van gebruikersaccounts. DeRegister
-actie is de enige die we in deze zelfstudie hebben gebruikt. Andere methoden in de klasse ondersteunen het opnieuw instellen van wachtwoorden, sociale aanmeldingen en andere functionaliteit. -
ApplicationUser
, gedefinieerd in /Models/IdentityModels.cs. Deze klasse is het EF-model voor gebruikersaccounts in de lidmaatschapsdatabase. -
ApplicationUserManager
, gedefinieerd in /App_Start/IdentityConfig.cs Deze klasse is afgeleid van UserManager- en voert bewerkingen uit op gebruikersaccounts, zoals het maken van een nieuwe gebruiker, het verifiëren van wachtwoorden, enzovoort, en wijzigingen in de database automatisch persistent maken. -
ApplicationOAuthProvider
. Dit object wordt aangesloten op de OWIN-middleware en verwerkt gebeurtenissen die door de middleware worden gegenereerd. Het is afgeleid van OAuthAuthAuthorizationServerProvider.
De autorisatieserver configureren
In StartupAuth.cs configureert de volgende code de OAuth2-autorisatieserver.
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);
De eigenschap TokenEndpointPath
is het URL-pad naar het eindpunt van de autorisatieserver. Dat is de URL die door de app wordt gebruikt om de bearer-tokens op te halen.
De eigenschap Provider
geeft een provider op die wordt aangesloten op de OWIN-middleware en verwerkt gebeurtenissen die door de middleware worden gegenereerd.
Dit is de basisstroom wanneer de app een token wil ophalen:
- Om een toegangstoken op te halen, verzendt de app een aanvraag naar ~/Token.
- De OAuth-middleware roept
GrantResourceOwnerCredentials
aan bij de provider. - De provider roept de
ApplicationUserManager
aan om hiermee de referenties te valideren en een claims-identiteit te creëren. - Als dat lukt, maakt de provider een verificatieticket dat wordt gebruikt om het token te genereren.
De OAuth-middleware weet niets over de gebruikersaccounts. De provider communiceert tussen de middleware en ASP.NET Identity. Zie OWIN OAuth 2.0 Authorization Servervoor meer informatie over het implementeren van de autorisatieserver.
Web-API configureren voor het gebruik van Bearer-tokens
In de WebApiConfig.Register
-methode stelt de volgende code verificatie in voor de web-API-pijplijn:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
De klasse HostAuthenticationFilter maakt verificatie mogelijk met bearer-tokens.
De methode SuppressDefaultHostAuthentication vertelt web-API om verificatie te negeren die plaatsvindt voordat de aanvraag de web-API-pijplijn bereikt, hetzij door IIS of door OWIN middleware. Op die manier kunnen we web-API beperken tot verificatie alleen met bearer-tokens.
Notitie
In het bijzonder kan het MVC-gedeelte van uw app formulierverificatie gebruiken, waarin referenties in een cookie worden opgeslagen. Verificatie op basis van cookies vereist het gebruik van antivervalsingstokens om CSRF-aanvallen te voorkomen. Dat is een probleem voor web-API's, omdat de web-API geen handige manier is om het antivervalsingstoken naar de client te verzenden. (Zie CSRF-aanvallen voorkomen in web-API-voor meer achtergrondinformatie over dit probleem.) Het aanroepen van SuppressDefaultHostAuthentication zorgt ervoor dat web-API niet kwetsbaar is voor CSRF-aanvallen van referenties die zijn opgeslagen in cookies.
Wanneer de client een beveiligde resource aanvraagt, gebeurt dit in de web-API-pijplijn:
- De HostAuthentication filter roept de OAuth-middleware aan om het token te valideren.
- De middleware converteert het token naar een claimidentiteit.
- Op dit moment wordt de aanvraag geauthentiseerd maar niet geautoriseerd.
- Het autorisatiefilter onderzoekt de claimidentiteit. Als de claims de gebruiker machtigen voor die resource, wordt de aanvraag geautoriseerd. De [Autoriseren] kenmerk autoriseert standaard elke aanvraag die is geverifieerd. U kunt echter autoriseren door rol of door andere claims. Zie verificatie en autorisatie in web-API-voor meer informatie.
- Als de vorige stappen zijn geslaagd, retourneert de controller de beveiligde resource. Anders ontvangt de client een 401-fout (niet geautoriseerd).
Aanvullende informatiebronnen
- ASP.NET Identity
- Inzicht in beveiligingsfuncties in de SPA-sjabloon voor VS2013 RC. MSDN-blogpost door Hongye Sun.
- het verwijderen van de sjabloon voor afzonderlijke accounts van de web-API, deel 2: lokale accounts. Blogbericht van Dominick Baier.
-
Verificatie van de host en Web API met OWIN. Een goede uitleg van
SuppressDefaultHostAuthentication
enHostAuthenticationFilter
door Brock Allen. - Profielinformatie aanpassen in ASP.NET Identity in VS 2013-sjablonen. MSDN-blogbericht door Pranav Rastogi.
-
Levensduurbeheer per aanvraag voor de klasse UserManager in ASP.NET Identity. MSDN-blogpost door Suhas Joshi, met een goede uitleg van de
UserManager
klasse.