PKCE pour sécuriser OAuth 2 en app native mobile ?

L’architecture applicative s’est convertie aux services web. Les applications n’accèdent plus aux ressources directement, mais par le biais d’API. Or, ces API ont besoin d’une donnée essentielle, l’identité de l’utilisateur qui est derrière l’application, et qui n’est donc pas visible directement. La personne est authentifiée auprès de l’application, pas auprès de l’API qui de toute façon ne peut recueillir aucune donnée de l’utilisateur derrière l’appareil. Ce qui d’ailleurs, impacte négativement le système de sécurité. L’appareil utilisé étant généralement dans notre cas, du système Iphone ou Android.
Si l’application se contente d’envoyer un identifiant à l’API, l’usurpation d’identité est facile, surtout dans le cas de ce qu’on appelle les clients publics, donc lorsque l’API est une donnée disponible sur internet. Cela est possible car, le client est en Javascript dans un navigateur internet ou, s’il est sur une app installée sur un smartphone.
La norme OAuth 2 a été conçue pour sécuriser la transmission des identités aux API. Elle est largement utilisée par les grands acteurs internet (Google, Microsoft, Amazon, etc.), et a donc prouvé que son système de sécurité est performant. D’autre part, elle a demandé un complément pour les app natives sur smartphone Android, Iphone ou autre. Ce complément a été ensuite appliqué à une application web à page unique (SPA).

Problématique pour les applications web monopage (SPA)

Afin d’être applicable dans la variété des cas d’usage d’applications, OAuth 2 est décliné en cinématiques (grant types) pour traiter tant les applications web classiques que les applications à page unique (SPA) ou même les applications lourdes directement installées sur les postes de travail.
La norme OAuth 2 a dédié une cinématique aux applications à page unique, qui s’appelle Implicit. C’est une cinématique très simplifiée, afin d’optimiser la réactivité des applications :

  • Le visiteur charge l’application sur son navigateur, qui commence par faire une redirection vers le serveur d’authentification.
  • Celui-ci authentifie le visiteur par la méthode de son choix (mot de passe, biométrie, etc.), génère un jeton d’accès très à cheval sur la sécurité (access token) et redirige le visiteur vers l’application en ajoutant le jeton
  • L’application détecte la présence du jeton, qu’elle utilise pour appeler les API
  • L’API valide le jeton et en extrait l’identité de l’utilisateur de l’application

Cette manière de procéder pose d’important problèmes de sécurité, car le jeton est une donnée transmise à l’application directement dans l’URL de retour, dans un fragment. Tous les équipements réseaux intermédiaires (proxy en particulier) ont donc accès à cette donnée critique.
Les applications web classiques avaient déjà une cinématique bénéficiant d’une meilleure sécurité (Authorization code détaillée juste après.) mais inutilisable en SPA, car elle nécessite un appel en service web au serveur d’authentification. Et à l’époque sans CORS (Cross-Origin Resource Sharing), cet appel était bloqué.

Problématique pour les app mobiles natives

Pour les app mobiles natives, la norme conseillait au départ le cinématique code d’autorisation (authorization code) qui est la suivante

  • avant un accès à une API, l’app ouvre le navigateur de l’appareil mobile et le pointe vers un serveur d’authentification (authorization server en dialecte OAuth 2).
  • celui-ci authentifie la personne et retourne un code valide uniquement durant une période donnée.
  • ce code temporaire est récupéré par l’app native, qui procède immédiatement à une connexion avec le serveur d’authentification, et qui l’échange contre un jeton d’accès sécurisé (access token).
  • l’app native fait son appel à l’API en lui transmettant le jeton.
  • l’API valide le jeton et en extrait l’identité de la personne donnée.

Evidemment, la sécurité du système repose sur la phase de validation du jeton d’accès, qui se fait soit par introspection (présentation au serveur d’authentification pour confirmation), soit par vérification de signature électronique.

La cinématique présentée s’appelle « code d’autorisation », car le serveur d’authentification ne retourne pas directement le jeton. On comprend pourquoi : il serait alors facilement récupérable et utilisable par autrui, il ne serait donc pas en sécurité. Il reste cependant que le code aussi peut être intercepté.
En application web classique, basée sur un serveur web, l’échange est sécurisé par authentification de l’application qui doit fournir un mot de passe pour échanger le code contre un jeton.
Pour les app mobiles, ce n’est pas pensable. Il faudrait en effet intégrer le mot de passe dans l’app, qui est publiée dans un app store et déployée potentiellement sur des millions d’appareils Android ou Iphone. On imagine bien qu’il serait facile de récupérer le mot de passe fourni par l’application.
Selon le schéma en notre possession ici, on devrait alors échanger le code contre le jeton sans authentification, ce qui ne favorise pas plus la sécurité que de transmettre directement le jeton, puisqu’en interceptant le code, il est aisé d’obtenir un jeton et de le présenter directement à l’API pour faire des opérations illicites.

Les bonnes fées PKCE à la rescousse

La vulnérabilité que l’on vient de décrire dans le cadre des app mobiles natives ayant été exploitée dans la vraie vie, une solution a rapidement été présentée par un complément de la cinématique code d’autorisation : Proof Key for Code Exchange by OAuth Public Clients, abrévié en PKCE et prononcé « Pixy » (fée en anglais).
Le principe général est de générer un mot de passe temporaire qui ne sera connu que de l’app native mobile et du serveur d’authentification. Donc même si un attaquant intercepte le code d’autorisation, il ne disposera pas du mot de passe pour l’échanger contre un jeton d’accès.
PCKE se présente comme suit :

  • avant d’orienter la personne vers le serveur d’authentification, l’app génère un secret, qu’elle ne divulgue pas. Elle en dérive une version transformée qu’elle transmet au serveur d’authentification
  • ce dernier réceptionne le secret transformé et le conserve, puis il poursuit avec les opérations habituelles d’authentification de la personne et de retour vers l’app avec le code
  • l’app récupère le code, et se connecte au serveur d’authentification pour obtenir un jeton en échange. Lors de cette connexion, elle fournit le secret (non transformé)
  • il ne reste plus qu’au serveur d’authentification de prendre ce secret, d’appliquer la transformation et de vérifier que le résultat correspond bien à ce qui avait transmis avec la demande initiale

On a donc pu rétablir une authentification de l’app grâce à un mot de passe dynamique.

Cette manière, de procéder n’était à l’époque pas généralisable aux applications web monopages :

  • le principe d’origine unique (same-origin policy) interdisait l’appel direct au serveur d’authentification pour échange du jeton contre le code.
  • les navigateurs ne savaient pas tous conserver le secret entre la demande et le retour d’authentification.

Depuis, avec CORS (Cross-Origin Resource Sharing) et LocalStorage, ces deux contraites tombent et PKCE s’impose comme la bonne pratique en SPA.
D’ailleurs la norme OAuth 2 impose PKCE depuis fin 2018, tant pour les app mobiles natives que pour les applications SPA, et a profité pour rendre la cinématique Implicit obsolète.

Utiliser OpenID Connect pour l’authentification

OAuth 2 ne donne aucune indication sur comment authentifier la personne utilisant l’application.
La norme OpenID Connect comble cette omission. Elle se base même pour cela sur les cinématiques OAuth 2 ! Elle dépasse le cadre strict OAuth 2 puisqu’elle fournit deux jetons : non seulement un jeton d’accès pour les API, mais aussi un jeton d’identité pour que l’application obtienne elle-même l’identité de l’utilisateur de l’appareil.
On verra dans une prochaine entrée comment l’utiliser.