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 tutoriel

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 d’organisation. 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.

Pour plus d’informations sur ces options, consultez Création de projets web ASP.NET dans Visual Studio 2013.

Les comptes individuels permettent à un utilisateur de se connecter de deux façons :

  • 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 aux réseaux sociaux. 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 locale. 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 montrer 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 readme 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, vous n’avez donc pas besoin de savoir Knockout.js pour cet article.

En cours de route, je vais décrire :

  • Ce que l’application fait 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. (Généralement 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 fournit des jetons d’accès.

Une application peut agir à la fois comme 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 Autorisation de la requête HTTP.

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

Lorsque vous sélectionnez 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 [Autoriser], toutes les demandes adressées à ce contrôleur ou à cette action doivent être authentifiées. Dans le cas contraire, 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 de 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 demande terminée, vous devriez voir un message d’erreur dans la zone Résultat . 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 du porteur, et donc pas d’en-tête d’autorisation dans la demande. Cela entraîne le retour d’une erreur 401 dans la demande.

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éer une application web MVC 5 sécurisée ASP.NET avec connexion, confirmation par e-mail et réinitialisation du mot de passe.) Pour le mot de passe, utilisez quelque chose comme « Password1! », avec une lettre majuscule, une lettre minuscule, un nombre et un caractère non alphanumérique. Pour simplifier l’application, j’ai exclu la validation côté client. En cas de problème avec le format du mot de passe, vous obtiendrez une erreur 400 (requête incorrecte).

Image d’inscrire une section utilisateur

Le bouton Inscrire envoie une requête POST à ~/api/Account/Register/. Le corps de la demande 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’appartenances.

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 Explorer serveur, 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 voyons maintenant le serveur d’autorisation OAuth en action lorsque nous demandons un jeton d’accès. Dans la zone Se connecter de l’exemple d’application, entrez l’adresse e-mail et le mot de passe, puis cliquez sur Se connecter.

Image de la section connexion

Le bouton Se connecter envoie une requête au point de terminaison du jeton. Le corps de la requête contient les données encodées sous forme url suivantes :

  • grant_type : « mot de passe »
  • nom d’utilisateur : <adresse 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, pour l’utiliser ultérieurement lors de l’envoi de requêtes à l’API. Contrairement à certaines formes d’authentification (telles que 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 cela limite les vulnérabilités CSRF.

Requête 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!

Vous pouvez voir que la requête contient les informations d’identification de l’utilisateur. Vous devez utiliser HTTPS pour assurer la sécurité de la 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 plus de lisibilité, j’ai mis en retrait le JSON et tronqué le jeton d’accès, qui est assez long.

Les access_tokenpropriétés , token_typeet expires_in sont définies par la spécification OAuth2. Les autres propriétés (userName, .issued, et .expires) sont uniquement à titre 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 à 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 ceci.

Image après un clic sur le bouton A IP d’appel

Requête 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

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 consiste simplement à « oublier » le jeton, en le 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 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 classes d’application main 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 tutoriel. 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’appartenances.
  • ApplicationUserManager, définie 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 à l’intergiciel OWIN et traite les événements déclenchés par l’intergiciel. Il dérive de OAuthAuthorizationServerProvider.

Image de main classes d’application

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 que l’application utilise 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 requête à ~/Token.
  2. L’intergiciel OAuth appelle GrantResourceOwnerCredentials sur le fournisseur.
  3. Le fournisseur appelle le ApplicationUserManager pour valider les informations d’identification et créer une identité de revendication.
  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 des comptes d’utilisateur. Le fournisseur communique entre l’intergiciel et ASP.NET Identity. Pour plus d’informations sur l’implémentation du serveur d’autorisation, consultez Serveur d’autorisation OWIN OAuth 2.0.

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 n’atteigne le pipeline d’API web, par IIS ou par l’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.

Notes

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 provenant d’informations d’identification stockées dans les 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 revendication.
  3. À ce stade, la demande est authentifiée , mais 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 supplémentaires