Une analyse cruciale de la rotation des jetons d’actualisation dans les applications Single-page

18 mars 2021
-minutes de lecture
Expert en sécurité web, Fondateur de Pragmatic Web Security

Introduction

Les applications Web modernes sont typiquement mises en œuvre comme des applications single-page, ou SPA, utilisant des technologies telles que Angular, React ou Vue. Une application single-page est déployée en tant que code HTML, CSS et JavaScript, et s’exécute dans le navigateur de l’utilisateur.

 

La spécification OAuth 2.0 originale stipulait que les applications Web front end devaient utiliser le flux implicite. En mars 2019, le OAuth 2.0 Security Best Current Practice a abandonné le flux implicite au profit du flux Authorization Code avec PKCE (Proof Key for Code Exchange). Sans entrer ici dans les détails, les applications Web front end peuvent désormais utiliser un flux Authorization Code, permettant à ces applications d’obtenir des jetons d’actualisation.

 

Incroyable, n’est-ce pas ?
 

En fait, la manipulation de jetons d’actualisation dans le navigateur a quelques implications en termes de sécurité. Dans cet article, nous étudions les propriétés de sécurité des jetons d’actualisation dans le navigateur. Nous analysons les raisons pour lesquelles les applications Web front end ont besoin de la rotation des jetons d’actualisation et ce que celle-ci apporte. Ensuite, nous explorons des scénarios d’attaques concrets qui contournent la rotation des jetons d’actualisation et évoquons comment les SPA sensibles doivent utiliser un backend-for-frontend pour sécuriser les jetons.

 

Commençons par regarder plus attentivement les jetons d’actualisation dans le navigateur.

Les jetons d’actualisation dans le navigateur

En utilisant un flux Authorization Code avec PKCE, une application Web front end peut demander des jetons d’identité, des jetons d’accès et des jetons d’actualisation. Avec un jeton d’actualisation, l’application frontend peut rapidement obtenir de nouveaux jetons d’accès. De ce fait, le serveur d’autorisation peut réduire la durée de vie des jetons d’accès à cinq ou dix minutes. Cela permet de réduire la fenêtre potentielle d’abus pour les jetons d’accès volés.

 

De l’autre côté, les applications Web frontend sont des clients publics, qui ne peuvent pas gérer les identifiants des clients de manière sécurisée. Par conséquent, ces clients ne peuvent pas s’authentifier sur le serveur d’autorisation lors d’un flux OAuth 2.0. Cette limite impacte la sécurité de l’échange de code, lorsqu’un client échange un code d’autorisation contre des jetons. Sans authentification du client, il n’existe pas de garantie que le client légitime soit en train d’échanger le code d’autorisation.

 

Les applications mobiles et natives soufrent du même problème. Pour ces clients, les spécifications OAuth 2.0 introduisent un concept connu comme PKCE, ou Proof Key for Code Exchange, Clé de vérification pour l’échange de code. En bref, la PKCE aide à s’assurer que seul le client qui lance le flux de code d’autorisation puisse échanger le code d’autorisation avec le serveur d’autorisation.

 

Ce mécanisme de PKCE, créé originellement pour les applications mobiles, s’applique aussi aux applications basées sur des navigateurs. En utilisant une PKCE, une application basée sur le navigateur peut garantir l’intégrité de l’étape d’échange de code dans le flux de code d’autorisation et éviter d’autres attaques, telles que des injections de codes.

 

Mais qu’en est-il du flux d’un jeton d’actualisation ? Lorsqu’ils utilisent un jeton d’actualisation, les clients privés doivent aussi s’authentifier. Les clients public, tels que les applications basées sur le navigateur, ne s’authentifient pas pendant le flux du jeton d’actualisation. Aussi, dans une application frontend typique, les jetons d’actualisation délivrés aux applications Web frontend sont des jetons porteurs.

 

En pratique, cela signifie que si un attaquant réussit à voler un jeton d’actualisation depuis une applications frontend, il pourra utiliser ce jeton dans le flux d’un jeton d’actualisation. Pour contrer ces attaques, les spécifications OAuth 2.0 obligent les applications basées sur un navigateur à appliquer une mesure de sécurité connue comme la rotation des jetons d’actualisation.

 

Avant de parler en détail de la rotation des jetons d’actualisation, évoquons d’abord la menace la plus courante visant les applications Web frontend.

Le besoin d’avoir la rotation des jetons d’actualisation

Les applications Web frontend sont conçues en utilisant HTML et JavaScript et s’exécutent dans le navigateur de l’utilisateur. Cette application frontend fonctionne comme une application client OAuth 2.0 sans s’appuyer sur un composant backend. Ce schéma permet aux applications frontend d’utiliser des jetons d’accès pour accéder directement aux API. Malheureusement, cela signifie que l’application frontend est la seule responsable du stockage et de la manipulation des jetons d’accès et des jetons d’actualisation.

 

L’une des plus grandes difficultés pour sécuriser les applications Web frontend est d’empêcher l’exécution d’un code JavaScript malveillant. La capacité à exécuter un code malveillant dans le contexte d’une application frontend permet à l’attaquant de manipuler le comportement de l’application. Une conséquence courante d’une telle attaque est le vol de jetons auprès du client. Étant donné que les jetons d’accès et les jetons d’actualisation sont des jetons porteurs, rien n’empêche un attaquant d’abuser de ces jetons volés.

 

Alors, comment un code JavaScript malveillant arrive-t-il jusqu’à une application frontend ?

 

Malheureusement, de nombreux vecteurs d’attaque peuvent conduire à l’exécution d’un code malveillant dans une application frontend. Une attaque connue comme Cross-Site Scripting (XSS). Dans une telle attaque, l’attaquant peut fournir des données malveillantes à l’application, qui rend ces données dans le navigateur de l’utilisateur (par ex. afficher un commentaire sur un restaurant émis par un autre utilisateur). Si l’application rend les données de manière non sécurisée, un commentaire sur un restaurant réalisé de manière malveillante peut entraîner l’exécution d’un code JavaScript. À travers cette vulnérabilité, l’attaquant peut injecter un code qui extrait des jetons du navigateur de l’utilisateur.

 

Un autre vecteur d’attaque s’appuie sur l’inclusion d’un contenu à distance ou de fichiers de codes. De nombreuses applications modernes chargent des bibliothèques JavaScript depuis des CDN (Content Delivery Networks), chargent des services tiers en incluant un fichier JavaScript et chargent des publicités depuis des fournisseurs publicitaires. Si l’attaquant compromet l’un de ces contenus inclus à distance, le code malveillant s’exécutera dans l’application de la victime. En compromettant le contenu à distance, l’attaquant trouve un point d’ancrage dans l’application. Les attaques réelles utilisent souvent ce schéma pour réaliser des fraudes à la carte bancaire à l’aide de malwares avec skimming.

 

Un troisième vecteur d’attaque découle de la manière dont les applications modernes sont développées. Chaque application Web s’appuie virtuellement sur des modules et des bibliothèques tiers chargés à partir de registres tels que des NPM. Si l’une de ces dépendances contient un code malveillant, ce code sera inclus dans le bouquet de l’application. Des histoires effrayantes tirées de situations réelles évoquent des attaques ciblées contre des applications à l’aide de modules NPM spécifiques.

 

Quels que soient les détails, ces vecteurs d’attaques ont tous une chose en commun : des résultats d’exploitation réussie dans l’exécution d’un code malveillant au niveau du navigateur de l’utilisateur. Pire, le code malveillant fonctionne dans le contexte d’exécution de l’application. Du point de vue du navigateur, il n’existe pas de différence entre le code malveillant et le code de l’application légitime. De ce fait, le code malveillant peut faire tout ce que l’application légitime peut faire.

 

Ramener cela à OAuth 2.0 et aux jetons d’actualisation met en lumière une faiblesse : Une fois qu’un code malveillant a été injecté dans l’application, il peut lire et extraire les jetons de l’application. Étant donné que les jetons sont effectivement des jetons porteurs, la présence d’un code malveillant pose un problème significatif.

Présenter la rotation des jetons d’actualisation

La menace du vol de jeton est bien connue dans le monde OAuth. Par conséquent, les spécifications OAuth 2.0 reconnaissent le danger des jetons d’actualisation porteurs dans les applications Web frontend. Ces spécifications OAuth 2.0 demandent des mesures de sécurité supplémentaires pour les jetons d’actualisation des clients publics, afin de limiter ce problème.

 

Une option est d’utiliser les jetons sender constrained. Ces jetons sont liés à un secret que seul le client connaît. Lorsqu’il utilise un tel jeton, l’émetteur doit prouver qu’il possède le secret. Sans cette preuve, le récepteur rejettera le jeton. Un exemple d’application est un client public fonctionnant sur un appareil mobile. Un tel client peut utiliser l’authentification mTLS, où le secret est stocké en sécurité sur l’appareil. Un autre exemple est l’utilisation de la DPoP, ou Demonstration of Proof-of-Possession, démonstration de preuve de possession. La DpoP est un mécanisme situé au niveau de l’application, qui demeure expérimental.

 

La seconde option est l’utilisation d’une « rotation des jetons d’actualisation ». Étant donné que les applications Web frontend ne peuvent pas utiliser facilement les jetons Sender Constrained Tokens, le conseil est d’avoir recours à la rotation des jetons d’actualisation pour les applications frontend. Lorsque la rotation du jeton d’actualisation est activée pour un client les jetons d’actualisation ne peuvent être utilisés qu’une seule fois. À chaque fois que le client utilise un jeton d’actualisation, le serveur d’autorisation délivre un nouveau jeton d’accès et un nouveau jeton d’actualisation. Lorsqu’un client veut lancer un nouveau flux de jeton d’actualisation, il utilise le jeton d’actualisation qui a été délivré en dernier. La chronologie ci-dessous illustre ce concept :

 

Lorsqu’un client utilise un jeton d’actualisation, il reçoit toujours un nouveau jeton d’actualisation pour la fois suivante. Par conséquent, les jetons d’actualisation sont utilisés une seule fois.

 

Toutefois, quand un attaquant utilise un code JavaScript malveillant pour voler le jeton d’actualisation du client, quelque chose d’intéressant se produit. L’application client ne sait pas que le jeton d’actualisation a été volé, donc elle continue d’utiliser le jeton d’actualisation pour obtenir de nouveaux jetons d’accès (et jetons d’actualisation). L’attaquant, qui a volé un jeton d’actualisation, veut également un nouveau jeton d’accès (et un jeton d’actualisation). De ce fait, soit l’attaquant soit l’application utilisera une deuxième fois un jeton d’actualisation, comme illustré dans la chronologie ci-dessous :

 

 

 

 

Dans ces scénarios, la réutilisation d’un jeton d’actualisation déclenche tous les types d’alarmes avec le serveur d’autorisation. La réutilisation des jetons d’actualisation signifie qu’une seconde partie essaye d’utiliser un jeton d’actualisation volé. En réponse à cette réutilisation, le serveur d’autorisation révoque immédiatement le jeton d’actualisation réutilisé, ainsi que tous les jetons qui en découlent. Concrètement, tous les jetons d’actualisation découlant du jeton d’actualisation réutilisé deviennent invalides.

 

Cette mesure semble drastique mais empêche efficacement d’autres abus de jetons volés. La rotation des jetons d’actualisation associée à la détection de la réutilisation des jetons d’actualisation augmente sensiblement la sécurité des jetons d’actualisation porteurs.

 

Mais est-ce suffisant pour sécuriser les jetons dans le navigateur ?

Contourner la rotation des jetons d’actualisation

Avant de plonger dans des scénarios d’attaque concrets, revoyons les capacités des attaquants. Lorsque l’attaquant injecte un code JavaScript malveillant dans une application, ce code s’exécute dans le navigateur de l’utilisateur. Ce code est impossible à distinguer du code d’application légitime et possède les mêmes privilèges que l’application légitime.

 

Un scénario d’attaque simple entraîne le vol direct des jetons existants de l’application. C’est précisément le scénario que nous avons évoqué précédemment, auquel la rotation des jetons d’actualisation apporte une réponse. Mais que peut également faire l’attaquant ?

 

Ci-dessous, nous évoquons ces scénarios concrets d’attaque qui contournent ou esquivent la rotation des jetons d’actualisation. Chacun de ces scénarios peut être réalisé par un attaquant ayant la possibilité d’exécuter un code JavaScript malveillant dans le contexte de l’exécution de l’application.
 

Scénario 1 : Voler les jetons d’accès

On croit souvent à tort qu’un code Javascript malveillant ne peut réaliser qu’une seule opération. Rien n’empêche l’attaquant d’installer un listener permanent pour observer le client quand il reçoit un nouveau jeton d’accès. Ce listener peut facilement extraire chaque jeton d’accès reçu par l’application. Ce scénario est illustré dans l’image ci-dessous :

 

 

Dans ce scénario, il s’agit d’une attaque en ligne, où l’accès de l’attaquant disparaît quand l’utilisateur ferme l’application client.
 

Scénario 2 : Esquiver la rotation des jetons d’actualisation

Comme dans le précédent scénario, l’attaquant peut installer un listener pour extraire des jetons d’actualisation de l’application. Tant que l’attaquant évite d’utiliser les jetons d’actualisation volés, le mécanisme de détection du serveur d’autorisation ne sera pas déclenché. Après avoir extrait les jetons d’actualisation, l’attaquant surveille également l’activité de l’application client.

 

Lorsque l’attaquant détecte que le client est devenu inactif, il utilise le jeton d’actualisation le plus récent pour obtenir de nouveaux jetons. Il évitera ainsi être détecté par le serveur d’autorisation, vu que le client légitime n’est plus actif. Du point de vue du serveur d’autorisation, aucun comportement malveillant n’a lieu. Ce scénario est illustré dans l’image ci-dessous :

 

 

Veuillez noter que dans ce scénario, l’attaquant a accès au nom de l’utilisateur pendant, en absolu, toute la durée de vie de la chaîne du jeton d’actualisation. Cela peut aller jusqu’à 8 ou 12 heures pour de nombreuses applications.
 

Scénario 3 : Usurper l’identité du client légitime

Au lieu de voler quelque chose à l’application légitime, l’attaquant peut simplement usurper l’application. Le code malveillant réalise n’importe quelle opération que l’application légitime réalise. Par conséquent, le code malveillant peut envoyer des demandes à une API comme le ferait l’application légitime. Ces demandes porteront des jetons d’accès légitimes, tout comme les demandes envoyées par l’application légitime. Pour l’essentiel, l’attaquant peut faire des appels d’API arbitraires à travers l’application légitime.

 

Cette technique ne constitue pas un nouveau vecteur d’attaque. Les applications Web traditionnelles basées sur des sessions souffrent d’un problème similaire, connu sous le nom de « session riding » ou détournement de session.
 

Scénario 4 : Demander discrètement de nouveaux jetons

Le scénario final est le plus dangereux. Au lieu d’extraire les jetons de l’application ou d’usurper l’application client légitime, l’attaquant peut lancer un nouveau flux OAuth 2.0. Ce nouveau flux est exécuté par un iframe caché, ce qui le rend totalement invisible pour l’utilisateur. En coulisses, le flux basé sur un iframe est configuré comme suit :

 

  • L’identifiant client renvoie à l’application client légitime.

  • Le paramètre d’incitation est réglé sur « aucun » pour éviter l’interaction de l’utilisateur.

  • Le paramètre mode_réponse est réglé sur message_web pour que l’iframe renvoie le code d’autorisation au contexte de recherche principal.

 

Il est possible de réussir un flux dans un iframe lorsque l’utilisateur a une session authentifiée avec le serveur d’autorisation. Étant donné que les clients frontend demandent généralement aux utilisateurs de se connecter lors du lancement de l’application, la présence d’une telle session est très improbable.

 

En exécutant un flux silencieux dans un iframe, l’attaquant peut obtenir un nouveau lot de jetons. Ces jetons sont indépendants des jetons que le client légitime utilisait déjà. De ce fait, aucun jeton d’actualisation n’est réutilisé, ce qui rend cette attaque indétectable par le serveur d’autorisation. Ce scénario est illustré dans l’image ci-dessous :

 

 

Veuillez noter que dans ce scénario, un attaquant peut même contourner les mécanismes de sécurité les plus avancés, comme la Preuve de possession au niveau de l’application. Notez également que ce scénario donne à l’attaquant un accès complet au nom de l’utilisateur tant que le jeton d’actualisation demeure valide. Cela peut aller jusqu’à 8 ou 12 heures pour de nombreuses applications.

Garder les jetons hors du navigateur

Malheureusement, l’histoire nous montre que les applications Web frontend sont difficiles à sécuriser. XSS tourmente les applications Web depuis presque vingt ans. Alors que les structures modernes JavaScript l’améliorent un peu, il existe toujours de nombreux vecteurs d’attaque XSS potentiels dans les applications modernes. Ces récapitulatifs fournissent des précisions sur la sécurisation des applications React et Angular.

 

Alors le jeu est-il terminé ? Oui et non. 

 

Du point de vue de la sécurité, il est virtuellement impossible de sécuriser des jetons dans une application Web frontend. Un code JavaScript malveillant peut faire tout ce qu’une application fait, donc si une application peut accéder à des jetons, le code malveillant le peut également. Les schémas de sécurité, consistant par exemple à cacher les jetons d’actualisation dans un Web Worker, peuvent aider mais ne sont pas une solution définitive pour lutter contre les scénarios présentés ci-dessus.

 

Concrètement, les applications frontend non sensibles peuvent s’appuyer sur des jetons d’actualisation avec rotation des jetons d’actualisation. Par exemple, une application qui gère les commentaires sur un restaurant n’est pas considérée comme sensible, ce qui réduit l’impact d’une attaque XSS réussie. Pour les applications sensibles, ce conseil n’est pas valable. Par exemple, les applications qui gèrent des informations personnelles, des données de santé ou des opérations financières sont extrêmement sensibles. Dans ces applications, une attaque XSS réussie a un impact considérable.


C’est pourquoi les applications frontend sensibles devraient éviter de gérer des jetons d’actualisation dans le navigateur. Au lieu de cela, elles peuvent s’appuyer sur un schéma Backend for Frontend (BFF), où la gestion des jetons est reportée sur un composant minimaliste du côté du serveur. L’image ci-dessous illustre le concept d’un BFF :

 

 

Les BFF sont traditionnellement utilisés pour agréger plusieurs API en une seule API cohérente pour servir une application client. Dans notre scénario, le BFF assume aussi un rôle de sécurité minimal. Il accepte les demandes de l’application client, les enrichit avec des jetons d’accès OAuth 2.0, et transfère les demandes à l’API. De même, toute réponse de l’API est transférée à l’application client.

 

Comment un BFF fonctionne en pratique ? 
 

Les détails d’un BFF

Dans ce scénario, le BFF devient l’application client OAuth 2.0. Étant donné que le BFF fonctionne sur un système de backend, il peut être configuré comme un client privé. Le BFF est le client, donc il initialise le flux OAuth 2.0 qui circule dans le navigateur de l’utilisateur. Après la première étape du flux, le BFF reçoit un code d’autorisation, qu’il échange contre des jetons avec le serveur d’autorisation utilisant l’authentification client. Avec le jeton d’accès, le BFF peut transférer des demandes d’API. Avec le jeton d’actualisation, le BFF peut obtenir un nouveau jeton d’accès lorsque cela est nécessaire. Notez que pour utiliser un jeton d’actualisation, il faut que le BFF s’authentifie sur le serveur d’autorisation.

 

Un BFF est partagé entre des centaines voire des milliers d’exemples de clients, chacun fonctionnant au nom d’un utilisateur différent. Le BFF garde la trace de ces utilisateurs avec une session basée sur un cookie. Le BFF peut garder la session sur le serveur (par exemple dans une simple mémoire de stockage) ou la pousser vers le client (par exemple dans un objet de session chiffré). Le premier entraîne un BFF avec un statut, tandis que le second permet au BFF de ne pas avoir de statut. Les deux approches sont valables.

 

Notez que le BFF doit suivre les bonnes pratiques de sécurité des cookies pour garantir la sécurité de ce cookie. Concrètement, cela signifie que pour établir un cookie avec le nom « MyBFFCookie », l’en-tête suivant doit être utilisé : Set-Cookie: __Host-MyBFFCookie=…; Secure; HttpOnly; SameSite. Obtenir plus d’informations sur la sécurité des cookies.

 

Lorsque le client envoie une demande, le BFF utilise les informations de la session dans la demande pour récupérer les jetons de l’utilisateur. Si le jeton d’accès est toujours valide, le BFF peut transférer directement la demande. Si le jeton d’accès a expiré, le BFF utilise le jeton d’actualisation de l’utilisateur pour obtenir un nouveau jeton d’accès avant de transférer la demande.

 

Enfin, notez que du point de vue de l’utilisateur, rien ne change. L’expérience utilisateur entre une application client frontend et une application frontend soutenue par un BFF est identique.
 

Les avantages d’un BFF

Un backend-for-fronted offre des avantages de sécurité considérables par rapport aux applications basées sur des navigateurs. Le BFF agit comme le client OAuth 2.0, en lui permettant d’appliquer les bonnes pratiques de sécurité pour les clients privés. Concrètement, cela signifie que :

 

  • Le BFF doit s’authentifier auprès du serveur d’autorisation lorsqu’il échange un code d’autorisation ou un jeton d’actualisation.

  • Le BFF peut s’appuyer sur des mécanismes d’authentification solides basés sur des clés (par ex. mTLS).

  • Le BFF peut utiliser des jetons d’accès sender-constrained et des jetons d’actualisation sender-constrained.

  • Les jetons d’accès et les jetons d’actualisation ne sont jamais exposés au navigateur.

 

Mais qu’en est-il des codes JavaScript potentiellement malveillants fonctionnant dans l’application frontend ? Dans ce scénario, le code malveillant ne peut plus accéder aux jetons puisqu’ils ne sont disponibles que pour le BFF. Les mesures de sécurité des cookies (par ex. l’attribut HttpOnly) empêchent le code malveillant de voler la session avec le BFF.

 

Le code malveillant peut toujours modifier le comportement de l’application client. Concrètement, l’attaquant peut réaliser une attaque par détournement de session ou session riding, en envoyant des appels d’API malveillants à travers le BFF. Ces appels d’API ne peuvent pas être distingués des demandes légitimes envoyées par le client.

 

Toutefois, ici, le BFF contrôle tout. Par conséquent, le BFF peut limiter la surface d’API en empêchant le client d’accéder à certains terminaux. De plus, le BFF peut appliquer des schémas d’analyse du trafic pour détecter des comportements douteux. Il peut s’agir par exemple de détecter un nombre étonnement suspect d’opérations ou d’observer des opérations sensibles réalisées dans un ordre inattendu.

 

Enfin, il ne faut pas oublier que le BFF ne fait rien pour arrêter l’exécution du code malveillant. L’attaquant peut toujours extraire des informations sensibles ou réaliser des attaques par social engineering sur l’utilisateur. Le seul moyen d’empêcher ces attaques est de suivre des directives de codage sécurisé pour l’application frontend.

Conclusion

En bref, un JavaScript malveillant constitue une véritable menace pour les applications basées sur des navigateurs. Les charges utiles de JavaScript avancées peuvent contourner les mécanismes de sécurité existants pour voler des jetons, y compris avec la rotation des jetons d’actualisation et l’isolement des jetons d’actualisation.

 

Compte tenu de tout cela, la seule solution pour sécuriser une SPA sensible est d’utiliser un backend-for-frontend qui suive les bonnes pratiques de sécurité OAuth 2.0 pour les clients privés.

 

Les SPA non sensibles peuvent suivre les conseils OAuth 2.0 pour les applications basées sur un navigateur. Ces SPA gèrent des jetons directement et s’appuient sur la rotation des jetons d’actualisation pour détecter la réutilisation des jetons.

 

Mais plus important encore, il faut suivre les conseils de codage sécurité pour éviter que les JavaScript malveillants aient un point d’ancrage au sein de l’application.

 

Pour en savoir plus sur la sécurité de OAuth 2.0 et OpenID Connect dans les applications monopages, consultez les cours de Philippe en ligne ou contactez directement Philippe pour obtenir de l’aide avec vos applications.

Partager cet article:
Ressources connexes

Lancez-vous dès Aujourd'hui

Contactez-Nous

sales@pingidentity.com

Découvrez comment Ping peut vous aider à offrir des expériences sécurisées aux employés, partenaires et clients dans un monde numérique en constante évolution.