Partager via


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.

Image de l’exemple de formulaire

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.

  1. L’utilisateur entre un nom et un mot de passe dans le client.
  2. Le client envoie ces informations d’identification au serveur d’autorisation.
  3. Le serveur d’autorisation authentifie les informations d’identification et retourne un jeton d’accès.
  4. 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.

Diagramme du flux d’informations d’identification de connexion locale

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.

Diagramme lorsque des comptes individuels sont sélectionnés dans le web A P I

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.

Image du message d’erreur de résultat

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

Image de l’inscription d’une section utilisateur

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.

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

Image de la section 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_tokenpropriétés et expires_in les token_typepropriétés sont définies par la spécification OAuth2. Les autres propriétés (userName, .issuedet .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.

Image après l’appel du bouton A P I a été cliqué sur

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’action Register 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.

Image des classes d’application principales

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 :

  1. Pour obtenir un jeton d’accès, l’application envoie une demande à ~/Token.
  2. L’intergiciel OAuth appelle GrantResourceOwnerCredentials le fournisseur.
  3. Le fournisseur appelle le ApplicationUserManager fournisseur pour valider les informations d’identification et créer une identité de revendications.
  4. Si cela réussit, le fournisseur crée un ticket d’authentification, qui est utilisé pour générer le jeton.

Diagramme du flux d’autorisation

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 :

  1. Le filtre HostAuthentication appelle l’intergiciel OAuth pour valider le jeton.
  2. L’intergiciel convertit le jeton en identité de revendications.
  3. À ce stade, la demande est authentifiée, mais n’est pas autorisée.
  4. 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.
  5. 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é).

Diagramme du moment où le client demande une ressource protégée

Ressources complémentaires