Partager via


Utiliser gestion des API pour protéger les jetons d’accès dans les applications Single-Page

Cet article explique comment utiliser la Gestion des API Azure pour implémenter une architecture sans état pour une application de page unique JavaScript qui ne stocke pas de jetons dans la session du navigateur. Cette approche permet de protéger les jetons d’accès contre les attaques de script intersites (XSS) et d’empêcher l’exécution de code malveillant dans le navigateur.

Cette architecture utilise Gestion des API pour effectuer les tâches suivantes :

  • Implémenter un modèle Backends for Frontends qui obtient un jeton d'accès OAuth2 à partir de Microsoft Entra ID
  • Utiliser Advanced Encryption Standard AES pour chiffrer et déchiffrer le jeton d’accès
  • Stocker le jeton dans un HttpOnly cookie
  • Utilisez un proxy pour tous les appels d’API nécessitant une autorisation.

Étant donné que le back-end gère l’acquisition de jetons, aucun autre code ou bibliothèque, tel que Microsoft Authentication Library pour JavaScript (MSAL.js), est requis dans l’application monopage. Quand vous utilisez cette conception, aucun jeton n’est stocké dans la session du navigateur ou dans le stockage local. Le chiffrement et le stockage du jeton d’accès dans un HttpOnly cookie permettent de le protéger contre les attaques XSS . Le fait de le délimiter au domaine de l’API et de définir le paramètre SameSite sur Strict garantit que le cookie est envoyé automatiquement avec toutes les demandes internes d’API proxysées.

Architecture

Diagramme montrant une architecture qui ne stocke pas de jetons dans le navigateur.

Téléchargez un fichier Visio de cette architecture.

Flux de travail

  1. Un utilisateur sélectionne Se connecter dans l’application monopage.

  2. L’application monopage appelle le flux du code d’autorisation via une redirection vers le point de terminaison d’autorisation Microsoft Entra.

  3. Les utilisateurs s’authentifient eux-mêmes.

  4. Une réponse de flux de code d’autorisation qui inclut un code d’autorisation est redirigée vers le point de terminaison de rappel Gestion des API.

  5. La stratégie de Gestion des API échange le code d’autorisation contre un jeton d’accès en appelant le point de terminaison de jetons de Microsoft Entra.

  6. La stratégie Gestion des API redirige vers l’application et place le jeton d’accès chiffré dans un HttpOnly cookie.

  7. L’utilisateur appelle un appel d’API externe depuis l’application via un point de terminaison proxisé de Gestion des API.

  8. La stratégie Gestion des API reçoit la demande d’API, déchiffre le cookie et effectue un appel d’API en aval pour ajouter le jeton d’accès Authorization en tant qu’en-tête.

Composants

  • Microsoft Entra ID est un service de gestion des identités et des accès basé sur le cloud qui fournit des services d’identité, l’authentification unique et l’authentification multifacteur sur les charges de travail Azure. Dans cette architecture, Microsoft Entra ID authentifie les utilisateurs et émet des jetons d’accès.

  • API Management est une plateforme de gestion hybride et multicloud dédiée aux API dans tous les environnements. Gestion des API crée des passerelles API cohérentes et modernes pour les services principaux existants. Cette architecture utilise API Management pour implémenter un modèle Back-ends pour front-ends, qui acquiert des jetons d’accès à partir de Microsoft Entra ID et agit comme proxy pour les appels d’API.

  • L’hébergement de sites web statiques dans le Stockage Azure utilise le Blob Storage Azure. Il est idéal pour fournir une prise en charge de l’hébergement de sites web statiques dans les cas où vous n’avez pas besoin d’un serveur web pour afficher le contenu. Cette architecture utilise l’hébergement de sites web statiques pour héberger l’application à page unique. L’application monopage est une application JavaScript qui s’exécute dans le navigateur et appelle la passerelle API Management pour accéder à l’API back-end.

Détails du scénario

Les applications monopage sont écrites en JavaScript et s’exécutent dans le contexte d’un navigateur côté client. Dans cette implémentation, les utilisateurs peuvent accéder à n’importe quel code qui s’exécute dans le navigateur. Le code malveillant qui s’exécute dans le navigateur ou une attaque XSS peut également accéder aux données. Les données stockées dans la session du navigateur ou le stockage local sont accessibles. Par conséquent, des données sensibles telles que des jetons d’accès peuvent être utilisées pour emprunter l’identité de l’utilisateur.

L’architecture décrite dans cet article augmente la sécurité des applications en déplaçant l’acquisition et le stockage de jetons vers le serveur principal et en utilisant un cookie chiffré HttpOnly pour stocker le jeton d’accès. Les jetons d’accès n’ont pas besoin d’être stockés dans la session du navigateur ou le stockage local, et le code malveillant qui s’exécute dans le navigateur ne peut pas y accéder.

Dans cette architecture, les stratégies de Gestion des API gèrent l’acquisition du jeton d’accès ainsi que le chiffrement et le déchiffrement du cookie. Les stratégies sont des collections d’instructions qui s’exécutent séquentiellement sur la requête ou la réponse d’une API et qui sont composées d’éléments XML et de scripts C#.

Le stockage du jeton dans un HttpOnly cookie permet de protéger le jeton contre les attaques XSS et de s’assurer qu’il n’est pas accessible par JavaScript. Le fait de délimiter le cookie au domaine de l’API et de définir le paramètre SameSite sur Strict garantit que le cookie est envoyé automatiquement avec toutes les demandes internes d’API proxysées. Cette conception permet d’ajouter automatiquement le jeton d’accès à l’en-tête Authorization de tous les appels d’API effectués par le serveur principal à partir de l’application monopage.

Étant donné que cette architecture utilise un cookie SameSite=Strict, le domaine de la passerelle API Management doit être identique au domaine de l’application monopage. Un cookie est envoyé à la passerelle Gestion des API uniquement lorsque la demande d’API provient d’un site dans le même domaine. Si les domaines sont différents, le cookie n’est pas ajouté à la demande d’API et la requête d’API proxysée reste non authentifiée.

Vous pouvez configurer cette architecture sans utiliser de domaines personnalisés pour l’instance Gestion des API et l’hébergement de sites web statiques. Dans ce scénario, vous devez utiliser SameSite=None pour le paramètre de cookie. Ce paramètre entraîne une implémentation moins sécurisée, car le cookie est ajouté à toutes les requêtes à n’importe quelle instance de la passerelle Gestion des API. Pour plus d’informations, consultez Cookies SameSite.

Pour en savoir plus sur l’utilisation de domaines personnalisés pour les ressources Azure, consultez Mapper un domaine personnalisé à un point de terminaison de stockage Blob et configurer un nom de domaine personnalisé pour votre instance Gestion des API. Pour plus d’informations sur la configuration des enregistrements DNS (Domain Name System) pour les domaines personnalisés, consultez Gérer les zones DNS dans le portail Azure.

Flux d’authentification

Ce processus utilise le flux de code d’autorisation OAuth2. Pour obtenir un jeton d’accès qui permet à l’application monopage d’accéder à l’API, les utilisateurs doivent d’abord s’authentifier eux-mêmes. Vous appelez le flux d’authentification en redirigeant les utilisateurs vers le point de terminaison d’autorisation Microsoft Entra. Vous devez configurer un identificateur de ressource uniforme (URI) pour la redirection dans Microsoft Entra ID. Cet URI de redirection doit être le point de terminaison de rappel de Gestion des API. Les utilisateurs sont invités à s’authentifier eux-mêmes en utilisant Microsoft Entra ID et sont redirigés vers le point de terminaison de rappel de Gestion des API avec un code d’autorisation. La stratégie de Gestion des API échange alors le code d’autorisation contre un jeton d’accès en appelant le point de terminaison de jetons de Microsoft Entra. Le diagramme suivant montre la séquence d’événements pour ce flux.

Diagramme montrant le flux d’authentification.

Le flux contient les étapes suivantes :

  1. Pour obtenir un jeton d’accès qui permet à l’application monopage d’accéder à l’API, les utilisateurs doivent d’abord s’authentifier eux-mêmes. Les utilisateurs appellent le flux en sélectionnant un bouton qui les redirige vers le point de terminaison d’autorisation de la plateforme d’identité Microsoft. Le redirect_uri est défini sur le point de terminaison d’API /auth/callback de la passerelle Gestion des API.

  2. Les utilisateurs sont invités à s’authentifier eux-mêmes. Si l’authentification réussit, la plateforme d’identités Microsoft répond avec une redirection.

  3. Le navigateur est redirigé vers le redirect_uri, qui est le point de terminaison de rappel de Gestion des API. Le code d’autorisation est passé au point de terminaison de rappel.

  4. La stratégie de trafic entrant du point de terminaison de rappel est appelée. La stratégie échange le code d’autorisation contre un jeton d’accès en émettant une requête vers le point de terminaison de jetons de Microsoft Entra. Il passe les informations nécessaires, comme l’ID client, la clé secrète client et le code d’autorisation :

    <send-request ignore-error="false" timeout="20" response-variable-name="response" mode="new">
     <set-url>https://login.microsoftonline.com/{{tenant-id}}/oauth2/v2.0/token</set-url>
     <set-method>POST</set-method>
     <set-header name="Content-Type" exists-action="override">
         <value>application/x-www-form-urlencoded</value>
     </set-header>
     <set-body>@($"grant_type=authorization_code&code={context.Request.OriginalUrl.Query.GetValueOrDefault("code")}&client_id={{client-id}}&client_secret={{client-secret}}&redirect_uri=https://{context.Request.OriginalUrl.Host}/auth/callback")</set-body>
    </send-request>
    
  5. Le jeton d’accès est retourné et stocké dans une variable nommée token :

    <set-variable name="token" value="@((context.Variables.GetValueOrDefault<IResponse>("response")).Body.As<JObject>())" />
    
  6. Le jeton d’accès est chiffré avec le chiffrement AES et stocké dans une variable nommée cookie :

    <set-variable name="cookie" value="@{
        var rng = new RNGCryptoServiceProvider();
        var iv = new byte[16];
        rng.GetBytes(iv);
        byte[] tokenBytes = Encoding.UTF8.GetBytes((string)(context.Variables.GetValueOrDefault<JObject>("token"))["access_token"]);
        byte[] encryptedToken = tokenBytes.Encrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv);
        byte[] combinedContent = new byte[iv.Length + encryptedToken.Length];
        Array.Copy(iv, 0, combinedContent, 0, iv.Length);
        Array.Copy(encryptedToken, 0, combinedContent, iv.Length, encryptedToken.Length);
        return System.Net.WebUtility.UrlEncode(Convert.ToBase64String(combinedContent));
     }" />
    
  7. La stratégie de trafic sortant du point de terminaison de rappel est appelée pour rediriger vers l’application monopage. Elle définit le jeton d’accès chiffré dans un cookie HttpOnly qui a SameSite défini sur Strict et est délimité au domaine de la passerelle Gestion des API. Aucune date d’expiration explicite n’est définie. Le cookie est donc créé en tant que cookie de session et expire lorsque le navigateur est fermé.

    <return-response>
        <set-status code="302" reason="Temporary Redirect" />
        <set-header name="Set-Cookie" exists-action="override">
            <value>@($"{{cookie-name}}={context.Variables.GetValueOrDefault<string>("cookie")}; Secure; SameSite=Strict; Path=/; Domain={{cookie-domain}}; HttpOnly")</value>
        </set-header>
        <set-header name="Location" exists-action="override">
            <value>{{return-uri}}</value>
        </set-header>
    </return-response>
    

Flux des appels d’API

Quand l’application monopage a le jeton d’accès, elle peut l’utiliser pour appeler l’API en aval. Le cookie est limité au domaine de l'application à page unique et est configuré avec l’attribut SameSite=Strict , ce qui le rend automatiquement ajouté à la requête. Le jeton d’accès peut ensuite être déchiffré afin de pouvoir être utilisé pour appeler l’API en aval. Le diagramme suivant montre la séquence d’événements pour ce flux.

Diagramme montrant la séquence d’appels d’API.

Le flux contient les étapes suivantes :

  1. Un utilisateur sélectionne un bouton dans l’application monopage pour appeler l’API en aval. Cette action appelle une fonction JavaScript qui appelle le point de terminaison d’API /graph/me de la passerelle Gestion des API.

  2. Étant donné que le cookie est limité au domaine de l’application monopage et a SameSite défini sur Strict, le navigateur ajoute automatiquement le cookie quand il envoie la demande à l’API.

  3. Quand la passerelle Gestion des API reçoit la requête, la stratégie de trafic entrant du point de terminaison /graph/me est appelée. La stratégie déchiffre le jeton d’accès du cookie et le stocke dans une variable nommée access_token :

    <set-variable name="access_token" value="@{
        try {
            string cookie = context.Request.Headers
                .GetValueOrDefault("Cookie")?
                .Split(';')
                .ToList()?
                .Where(p => p.Contains("{{cookie-name}}"))
                .FirstOrDefault()
                .Replace("{{cookie-name}}=", "");
            byte[] encryptedBytes = Convert.FromBase64String(System.Net.WebUtility.UrlDecode(cookie));
            byte[] iv = new byte[16];
            byte[] tokenBytes = new byte[encryptedBytes.Length - 16];
            Array.Copy(encryptedBytes, 0, iv, 0, 16);
            Array.Copy(encryptedBytes, 16, tokenBytes, 0, encryptedBytes.Length - 16);
            byte[] decryptedBytes = tokenBytes.Decrypt("Aes", Convert.FromBase64String("{{enc-key}}"), iv);
            char[] convertedBytesToChar = Encoding.UTF8.GetString(decryptedBytes).ToCharArray();
            return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(convertedBytesToChar));
        } catch (Exception ex) {
            return null;
        }
    }" />
    
  4. Le jeton d’accès est ajouté à la requête effectuée à l’API en aval sous la forme d’un en-tête Authorization :

    <choose>
        <when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("access_token")))">
            <set-header name="Authorization" exists-action="override">
                <value>@($"Bearer {context.Variables.GetValueOrDefault<string>("access_token")}")</value>
            </set-header>
        </when>
    </choose>
    
  5. La requête est transmise à l’API en aval, et le jeton d’accès est inclus dans l’en-tête Authorization.

  6. La réponse de l’API en aval est retournée directement à l’application monopage.

Déployer ce scénario

Pour obtenir des exemples complets de stratégies décrites par cet article, les spécifications OpenAPI et un guide de déploiement complet, consultez ce référentiel GitHub.

Améliorations

Cette solution n’est pas prête pour la production. Il est destiné à illustrer ce que vous pouvez faire à l’aide des services décrits par cet article. Tenez compte des facteurs suivants avant d’utiliser la solution en production.

  • Cet exemple n’implémente pas l’expiration des jetons d’accès, ou l’utilisation de jetons d’actualisation ou d’ID.

  • Le contenu du cookie dans l’exemple est chiffré via le chiffrement AES. La clé est stockée en tant que secret dans le volet Valeurs nommées de l’instance Gestion des API. Pour mieux protéger cette valeur nommée, vous pouvez utiliser une référence à un secret stocké dans Azure Key Vault. Vous devez permuter régulièrement les clés de chiffrement dans le cadre de votre stratégie de gestion des clés.

  • Cet exemple proxyse seulement les appels à une seule API en aval, de sorte qu’il ne nécessite qu’un seul jeton d’accès. Ce scénario autorise une approche sans état. Cependant, en raison de la limite de taille des cookies HTTP, si vous avez besoin de proxyser des appels à plusieurs API en aval, vous avez besoin d’une approche avec état.

    Au lieu d’utiliser un jeton d’accès unique, cette approche stocke les jetons d’accès dans un cache et les récupère en fonction de l’API appelée et d’une clé fournie dans le cookie. Vous pouvez implémenter cette approche en utilisant le cache de Gestion des API ou un cache Redis externe.

  • Cet exemple illustre la récupération de données uniquement par le biais d’une requête GET. Il ne fournit donc pas de protection contre les attaques par falsification de requête intersite (CSRF). Si vous utilisez d’autres méthodes HTTP, comme POST, PUT, PATCH ou DELETE, cette protection est nécessaire.

Contributeurs

Microsoft gère cet article. Les contributeurs suivants ont écrit cet article.

Auteur principal :

Pour afficher les profils LinkedIn non publics, connectez-vous à LinkedIn.

Étapes suivantes