Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 (Sécuriser une API Web avec des comptes individuels et une connexion locale dans API Web ASP.NET 2.2)
par Mike Wasson
Télécharger l’exemple d’application
Cette rubrique montre comment sécuriser une API web à l’aide d’OAuth2 pour s’authentifier auprès d’une base de données d’appartenance.
Versions logicielles utilisées dans le didacticiel
Dans Visual Studio 2013, le modèle de projet d’API web vous offre trois options d’authentification :
- Comptes individuels. L’application utilise une base de données d’appartenance.
- Comptes organisationnels. Les utilisateurs se connectent avec leurs informations d’identification Azure Active Directory, Office 365 ou Active Directory locales.
- Authentification Windows. Cette option est destinée aux applications Intranet et utilise le module IIS d’authentification Windows.
Les comptes individuels fournissent deux façons pour un utilisateur de se connecter :
- Connexion locale. L’utilisateur s’inscrit sur le site, en entrant un nom d’utilisateur et un mot de passe. L’application stocke le hachage de mot de passe dans la base de données d’appartenance. Lorsque l’utilisateur se connecte, le système ASP.NET Identity vérifie le mot de passe.
- Connexion sociale. L’utilisateur se connecte avec un service externe, tel que Facebook, Microsoft ou Google. L’application crée toujours une entrée pour l’utilisateur dans la base de données d’appartenance, mais ne stocke pas d’informations d’identification. L’utilisateur s’authentifie en se connectant au service externe.
Cet article examine le scénario de connexion local. Pour la connexion locale et sociale, l’API web utilise OAuth2 pour authentifier les demandes. Toutefois, les flux d’informations d’identification sont différents pour la connexion locale et sociale.
Dans cet article, je vais illustrer une application simple qui permet à l’utilisateur de se connecter et d’envoyer des appels AJAX authentifiés à une API web. Vous pouvez télécharger l’exemple de code ici. Le fichier lisez-moi explique comment créer l’exemple à partir de zéro dans Visual Studio.
L’exemple d’application utilise Knockout.js pour la liaison de données et jQuery pour l’envoi de requêtes AJAX. Je vais me concentrer sur les appels AJAX, donc vous n’avez pas besoin de savoir Knockout.js pour cet article.
Le long de la route, je vais décrire :
- Ce que fait l’application côté client.
- Ce qui se passe sur le serveur.
- Trafic HTTP au milieu.
Tout d’abord, nous devons définir une terminologie OAuth2.
- Ressource. Certaines données qui peuvent être protégées.
- Serveur de ressources. Serveur qui héberge la ressource.
- Propriétaire de la ressource. Entité qui peut accorder l’autorisation d’accéder à une ressource. (En général, l’utilisateur.)
- Client : application qui souhaite accéder à la ressource. Dans cet article, le client est un navigateur web.
- Jeton d’accès. Jeton qui accorde l’accès à une ressource.
- Jeton du porteur. Type particulier de jeton d’accès, avec la propriété que tout le monde peut utiliser le jeton. En d’autres termes, un client n’a pas besoin d’une clé de chiffrement ou d’un autre secret pour utiliser un jeton du porteur. Pour cette raison, les jetons du porteur ne doivent être utilisés que sur un protocole HTTPS et doivent avoir des délais d’expiration relativement courts.
- Serveur d’autorisation. Serveur qui donne des jetons d’accès.
Une application peut agir en tant que serveur d’autorisation et serveur de ressources. Le modèle de projet d’API web suit ce modèle.
Flux d’informations d’identification de connexion locale
Pour la connexion locale, l’API web utilise le flux de mot de passe du propriétaire de la ressource défini dans OAuth2.
- L’utilisateur entre un nom et un mot de passe dans le client.
- Le client envoie ces informations d’identification au serveur d’autorisation.
- Le serveur d’autorisation authentifie les informations d’identification et retourne un jeton d’accès.
- Pour accéder à une ressource protégée, le client inclut le jeton d’accès dans l’en-tête d’autorisation de la requête HTTP.
Lorsque vous sélectionnez Des comptes individuels dans le modèle de projet d’API web, le projet inclut un serveur d’autorisation qui valide les informations d’identification de l’utilisateur et émet des jetons. Le diagramme suivant montre le même flux d’informations d’identification en termes de composants d’API web.
Dans ce scénario, les contrôleurs d’API web agissent en tant que serveurs de ressources. Un filtre d’authentification valide les jetons d’accès et l’attribut [Authorize] est utilisé pour protéger une ressource. Lorsqu’un contrôleur ou une action a l’attribut [Authorize], toutes les demandes adressées à ce contrôleur ou action doivent être authentifiées. Sinon, l’autorisation est refusée et l’API web retourne une erreur 401 (non autorisée).
Le serveur d’autorisation et le filtre d’authentification appellent tous deux un composant middleware OWIN qui gère les détails d’OAuth2. Je vais décrire la conception plus en détail plus loin dans ce tutoriel.
Envoi d’une demande non autorisée
Pour commencer, exécutez l’application et cliquez sur le bouton Appeler l’API. Une fois la requête terminée, un message d’erreur doit s’afficher dans la zone Résultats . Cela est dû au fait que la demande ne contient pas de jeton d’accès, de sorte que la demande n’est pas autorisée.
Le bouton Appeler l’API envoie une requête AJAX à ~/api/values, qui appelle une action de contrôleur d’API web. Voici la section du code JavaScript qui envoie la requête AJAX. Dans l’exemple d’application, tout le code de l’application JavaScript se trouve dans le fichier 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);
Tant que l’utilisateur ne se connecte pas, il n’y a pas de jeton de porteur, et donc aucun en-tête d’autorisation dans la demande. Cela provoque une erreur 401 pour la requête.
Voici la requête HTTP. (J’ai utilisé Fiddler pour capturer le trafic 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/
Réponse 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."}
Notez que la réponse inclut un en-tête Www-Authenticate avec le défi défini sur Porteur. Cela indique que le serveur attend un jeton du porteur.
Inscrire un utilisateur
Dans la section Inscrire de l’application, entrez un e-mail et un mot de passe, puis cliquez sur le bouton Inscrire .
Vous n’avez pas besoin d’utiliser une adresse e-mail valide pour cet exemple, mais une application réelle confirmerait l’adresse. (Voir Créez une application web MVC 5 sécurisée ASP.NET avec la connexion, la confirmation par e-mail et la réinitialisation du mot de passe.) Pour le mot de passe, utilisez quelque chose comme « Mot de passe1 ! », avec une lettre majuscule, une lettre minuscule, un nombre et un caractère non alphanumérique. Pour simplifier l’application, j’ai laissé la validation côté client. Par conséquent, si le format de mot de passe pose problème, vous obtiendrez une erreur 400 (Demande incorrecte).
Le bouton Inscrire envoie une requête POST à ~/api/Account/Register/. Le corps de la requête est un objet JSON qui contient le nom et le mot de passe. Voici le code JavaScript qui envoie la requête :
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);
Requête HTTP, où $CREDENTIAL_PLACEHOLDER$
est un espace réservé pour la paire clé-valeur de mot de passe :
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$"}
Réponse HTTP :
HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0
Cette requête est gérée par la AccountController
classe. En interne, AccountController
utilise ASP.NET Identity pour gérer la base de données d’appartenance.
Si vous exécutez l’application localement à partir de Visual Studio, les comptes d’utilisateur sont stockés dans LocalDB, dans la table AspNetUsers. Pour afficher les tables dans Visual Studio, cliquez sur le menu Affichage , sélectionnez Explorateur de serveurs, puis développez Connexions de données.
Obtenir un jeton d’accès
Jusqu’à présent, nous n’avons pas effectué d’OAuth, mais nous verrons maintenant le serveur d’autorisation OAuth en action, lorsque nous demandons un jeton d’accès. Dans la zone Connexion de l’exemple d’application, entrez l’e-mail et le mot de passe, puis cliquez sur Se connecter.
Le bouton Se connecter envoie une demande au point de terminaison du jeton. Le corps de la requête contient les données encodées form-url suivantes :
- grant_type : « mot de passe »
- nom d’utilisateur : <e-mail de l’utilisateur>
- mot de passe : <mot de passe>
Voici le code JavaScript qui envoie la requête 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);
Si la demande réussit, le serveur d’autorisation retourne un jeton d’accès dans le corps de la réponse. Notez que nous stockons le jeton dans le stockage de session, à utiliser ultérieurement lors de l’envoi de requêtes à l’API. Contrairement à certaines formes d’authentification (par exemple, l’authentification basée sur les cookies), le navigateur n’inclut pas automatiquement le jeton d’accès dans les demandes suivantes. L’application doit le faire explicitement. C’est une bonne chose, car elle limite les vulnérabilités CSRF.
Requête HHTP :
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!
Vous pouvez voir que la demande contient les informations d’identification de l’utilisateur. Vous devez utiliser HTTPS pour fournir une sécurité de couche de transport.
Réponse 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"
}
Pour une lisibilité, j’ai mis en retrait le JSON et tronqué le jeton d’accès, ce qui est assez long.
Les access_token
propriétés et expires_in
les token_type
propriétés sont définies par la spécification OAuth2. Les autres propriétés (userName
, .issued
et .expires
) sont uniquement à des fins d’information. Vous trouverez le code qui ajoute ces propriétés supplémentaires dans la TokenEndpoint
méthode, dans le fichier /Providers/ApplicationOAuthProvider.cs.
Envoyer une demande authentifiée
Maintenant que nous avons un jeton du porteur, nous pouvons effectuer une demande authentifiée auprès de l’API. Pour ce faire, définissez l’en-tête d’autorisation dans la demande. Cliquez de nouveau sur le bouton Appeler l’API pour voir cela.
Requête HHTP :
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
Réponse 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
Étant donné que le navigateur ne met pas en cache les informations d’identification ou le jeton d’accès, la déconnexion est simplement une question d'« oubli » du jeton, en la supprimant du stockage de session :
self.logout = function () {
sessionStorage.removeItem(tokenKey)
}
Présentation du modèle de projet Comptes individuels
Lorsque vous sélectionnez Comptes individuels dans le modèle de projet d’application web ASP.NET, le projet inclut :
- Un serveur d’autorisation OAuth2.
- Point de terminaison d’API web pour la gestion des comptes d’utilisateur
- Modèle EF pour le stockage des comptes d’utilisateur.
Voici les principales classes d’application qui implémentent ces fonctionnalités :
AccountController
. Fournit un point de terminaison d’API web pour la gestion des comptes d’utilisateur. L’actionRegister
est la seule que nous avons utilisée dans ce didacticiel. D’autres méthodes de la classe prennent en charge la réinitialisation de mot de passe, les connexions sociales et d’autres fonctionnalités.ApplicationUser
, défini dans /Models/IdentityModels.cs. Cette classe est le modèle EF pour les comptes d’utilisateur dans la base de données d’appartenance.ApplicationUserManager
, défini dans /App_Start/IdentityConfig.cs Cette classe dérive de UserManager et effectue des opérations sur les comptes d’utilisateur, telles que la création d’un utilisateur, la vérification des mots de passe, etc. et conserve automatiquement les modifications apportées à la base de données.ApplicationOAuthProvider
. Cet objet se connecte au middleware OWIN et traite les événements déclenchés par l’intergiciel. Il dérive de OAuthAuthorizationServerProvider.
Configuration du serveur d’autorisation
Dans StartupAuth.cs, le code suivant configure le serveur d’autorisation 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
propriété est le chemin d’URL du point de terminaison du serveur d’autorisation. Il s’agit de l’URL utilisée par l’application pour obtenir les jetons du porteur.
La Provider
propriété spécifie un fournisseur qui se connecte au middleware OWIN et traite les événements déclenchés par l’intergiciel.
Voici le flux de base lorsque l’application souhaite obtenir un jeton :
- Pour obtenir un jeton d’accès, l’application envoie une demande à ~/Token.
- L’intergiciel OAuth appelle
GrantResourceOwnerCredentials
le fournisseur. - Le fournisseur appelle le
ApplicationUserManager
fournisseur pour valider les informations d’identification et créer une identité de revendications. - Si cela réussit, le fournisseur crée un ticket d’authentification, qui est utilisé pour générer le jeton.
L’intergiciel OAuth ne connaît rien sur les comptes d’utilisateur. Le fournisseur communique entre l’intergiciel et l’identité ASP.NET. Pour plus d’informations sur l’implémentation du serveur d’autorisation, consultez OWIN OAuth 2.0 Authorization Server.
Configuration de l’API web pour utiliser des jetons du porteur
Dans la WebApiConfig.Register
méthode, le code suivant configure l’authentification pour le pipeline d’API web :
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
La classe HostAuthenticationFilter active l’authentification à l’aide de jetons du porteur.
La méthode SuppressDefaultHostAuthentication indique à l’API web d’ignorer toute authentification qui se produit avant que la requête atteigne le pipeline d’API web, par IIS ou par intergiciel OWIN. De cette façon, nous pouvons limiter l’API Web pour qu’elle n’effectue l’authentification qu’à l’aide des jetons du porteur.
Remarque
En particulier, la partie MVC de votre application peut utiliser l’authentification par formulaire, qui stocke les informations d’identification dans un cookie. L’authentification basée sur les cookies nécessite l’utilisation de jetons anti-falsification pour empêcher les attaques CSRF. C’est un problème pour les API web, car il n’existe aucun moyen pratique pour l’API web d’envoyer le jeton anti-falsification au client. (Pour plus d’informations sur ce problème, consultez Prévention des attaques CSRF dans l’API web.) L’appel de SuppressDefaultHostAuthentication garantit que l’API web n’est pas vulnérable aux attaques CSRF à partir d’informations d’identification stockées dans des cookies.
Lorsque le client demande une ressource protégée, voici ce qui se passe dans le pipeline d’API web :
- Le filtre HostAuthentication appelle l’intergiciel OAuth pour valider le jeton.
- L’intergiciel convertit le jeton en identité de revendications.
- À ce stade, la demande est authentifiée, mais n’est pas autorisée.
- Le filtre d’autorisation examine l’identité des revendications. Si les revendications autorisent l’utilisateur pour cette ressource, la demande est autorisée. Par défaut, l’attribut [Authorize] autorise toute demande authentifiée. Toutefois, vous pouvez autoriser par rôle ou par d’autres revendications. Pour plus d’informations, consultez Authentification et autorisation dans l’API web.
- Si les étapes précédentes réussissent, le contrôleur retourne la ressource protégée. Sinon, le client reçoit une erreur 401 (non autorisé).
Ressources complémentaires
- ASP.NET Identity
- Présentation des fonctionnalités de sécurité dans le modèle SPA pour VS2013 RC. Billet de blog MSDN de Hongye Sun.
- Dissecting the Web API Individual Accounts Template–Part 2 : Local Accounts. Billet de blog de Dominick Baier.
- Authentification de l’hôte et API web avec OWIN. Une bonne explication de
SuppressDefaultHostAuthentication
EtHostAuthenticationFilter
par Brock Allen. - Personnalisation des informations de profil dans ASP.NET Identity dans les modèles VS 2013. Billet de blog MSDN de Pranav Rastogi.
- Gestion de la durée de vie des requêtes pour la classe UserManager dans ASP.NET Identity. Billet de blog MSDN de Suhas Joshi, avec une bonne explication de la
UserManager
classe.