Aller au contenu

FAPI 2.0 : ce qu'un profil de sécurité interdit, et pourquoi

FAPI 2.0 : ce qu’un profil de sécurité interdit, et pourquoi

Section intitulée « FAPI 2.0 : ce qu’un profil de sécurité interdit, et pourquoi »

OAuth 2.0 est un cadre, pas un protocole. Il laisse au déployeur une quantité remarquable de choix : quel type de réponse, quelle méthode d’authentification du client, quelle durée de vie, quel algorithme de signature, quelle forme de jeton. Chacun de ces choix peut être fait correctement, et il suffit qu’un seul soit fait au hasard pour que l’ensemble s’écroule. Un profil de sécurité est la réponse à ce problème, et sa technique n’est pas d’ajouter des contrôles mais d’interdire des options. Le profil FAPI 2.0 de la fondation OpenID, publié en version finale le 22 février 2025, est le plus intéressant du genre, parce qu’il justifie chacune de ses interdictions.

→ Lire l’explication complète de la marmotte

Le document se présente comme « un profil de sécurité élevé d’usage général d’OAuth 2.0 qui a été prouvé, par analyse formelle, satisfaire le modèle d’attaquant énoncé ». Ces deux éléments, l’analyse formelle et le modèle d’attaquant explicite, sont ce qui distingue un profil d’une liste de bonnes pratiques. Un modèle d’attaquant dit ce dont on prétend se défendre ; une analyse formelle, comme celles décrites dans la vérification de protocoles avec Tamarin ou ProVerif, démontre que la combinaison de mécanismes retenue atteint les objectifs annoncés dans ce modèle.

D’où la règle la plus importante du document pour qui voudrait l’adapter : « pour qu’un profil soit conforme à ce document, ce profil ne doit pas retirer ni surcharger les comportements obligatoires, puisque le faire est susceptible d’invalider l’analyse de sécurité formelle et de réduire la sécurité de manières potentiellement imprévisibles ». Autrement dit, on peut restreindre davantage, et le document y encourage, mais on ne peut pas assouplir : retirer une seule exigence ne retire pas « un peu » de sécurité, il annule la preuve. C’est une propriété désagréable et parfaitement réelle des raisonnements de ce type.

Dernier point de cadrage, utile à qui croirait le sujet réservé aux banques : « bien que le profil de sécurité ait été initialement développé avec un accent sur les applications financières, il est conçu pour être universellement applicable à la protection d’API exposant des données de grande valeur et sensibles ». Rien n’empêche une direction informatique d’adopter ce profil pour ses API internes à fort impact, et c’est probablement le meilleur usage qu’on puisse faire du document.

C’est la partie qui mérite d’être lue, parce qu’elle tient en une page et qu’elle reprend, par soustraction, une décennie d’erreurs observées.

Le profil rejette l’octroi par mot de passe du propriétaire de ressource, c’est-à-dire la pratique qui consiste à faire transmettre les identifiants de l’utilisateur au client. Il n’accepte que des clients confidentiels, et sa justification est sobre : « le groupe de travail n’a pas connaissance de mécanismes qui permettraient de sécuriser des clients publics au même degré, et leur usage n’entre donc pas dans le périmètre de ce document ». Il n’émet que des jetons contraints par l’expéditeur, par TLS mutuel selon la RFC 8705 ou par DPoP selon la RFC 9449, et ferme ainsi la porte au jeton au porteur nu : voir les jetons liés au certificat client et DPoP. Il n’accepte que deux méthodes d’authentification du client, TLS mutuel ou assertion signée par clé privée, ce qui élimine d’un trait le secret partagé transmis en en-tête ou dans le corps de la requête.

Côté point d’autorisation, la série est tout aussi tranchée. Le type de réponse doit valoir code, ce qui exclut le flux implicite et tous les hybrides. Les requêtes d’autorisation envoyées sans requête poussée sont rejetées, et les requêtes poussées sans authentification du client le sont aussi, double exigence détaillée dans PAR et JAR. PKCE est obligatoire, avec S256 comme seule méthode de défi, et le client doit générer un défi spécifique à chaque requête et le lier à l’agent utilisateur dans lequel le flux a démarré ; le mécanisme est décrit dans le flux OAuth avec PKCE en une page. Le serveur doit renvoyer un paramètre d’identification de l’émetteur dans la réponse d’autorisation, et le client doit le vérifier, pour empêcher les attaques de confusion de serveur. Les adresses de redirection en schéma http sont interdites, sauf pour les clients natifs utilisant une interface de bouclage local. Un code d’autorisation déjà utilisé doit être rejeté. Le code d’état HTTP 307 est proscrit pour la redirection d’une requête contenant des identifiants, « pour éviter de transmettre accidentellement les identifiants à un tiers », le 303 étant recommandé. Le point d’autorisation ne doit pas prendre en charge le partage de ressources entre origines, « puisque les clients doivent effectuer une redirection HTTP plutôt que d’accéder directement à ce point ». Et aucun des trois rôles ne doit exposer de redirecteur ouvert.

Côté serveur de ressources, deux exigences résument des années de mauvaise habitude : les jetons sont acceptés dans l’en-tête HTTP, et ne doivent pas être acceptés en paramètre de requête, ce qui les ferait entrer dans les journaux et dans l’en-tête de référent pour les raisons exposées dans la clé de cache HTTP. Et la ressource doit vérifier non seulement la validité, l’intégrité, l’expiration et l’état de révocation du jeton, mais aussi « que l’autorisation représentée par le jeton est suffisante pour l’accès demandé », ce qui paraît évident et qui est précisément ce que la confusion de scope OAuth exploite.

Un profil utile ne dit pas « une durée courte », il dit un nombre. FAPI 2.0 en fixe plusieurs, et il vaut mieux les connaître que les réinventer.

Les codes d’autorisation ont une durée de vie maximale de soixante secondes. Les adresses de requête poussée expirent en moins de six cents secondes, et le document explique pourquoi ce n’est pas plus court : « le délai doit être suffisant pour que l’appareil de l’utilisateur reçoive le lien et que l’utilisateur termine le processus d’ouverture du lien. Dans de nombreux cas, mauvaise connexion réseau ou nécessité de sélectionner manuellement le navigateur, cela peut facilement prendre plus de trente secondes. » Il ajoute une remarque d’exploitation qui sauve des heures de diagnostic : l’unicité d’usage de cette adresse doit être appliquée « au point d’autorisation, et non au point de chargement d’une page d’autorisation. Cela empêche les logiciels utilisateurs qui préchargent les adresses d’invalider la requête. »

Les secrets et jetons non destinés à être manipulés par un humain doivent être créés « avec au moins 128 bits d’entropie, de telle sorte qu’un attaquant devinant correctement la valeur soit informatiquement infaisable », exigence qui renvoie aux propriétés de produire de l’aléa utilisable. Les clés RSA font au minimum 2048 bits, les clés sur courbe elliptique au minimum 224 bits, et seuls trois algorithmes de signature sont autorisés, PS256, ES256 et EdDSA dans sa variante Ed25519, l’algorithme none étant explicitement refusé ; le cadre de ces choix est celui de choisir, combiner et migrer les algorithmes et de JWS.

Le traitement du décalage d’horloge est le plus instructif, parce qu’il montre un profil raisonnant sur le comportement réel des implémenteurs. Le serveur doit accepter les jetons dont l’horodatage est « entre 0 et 10 secondes dans le futur » et doit rejeter ceux au-delà de soixante secondes. La justification tient en une phrase, et c’est la plus honnête du document : « même quelques centaines de millisecondes de décalage d’horloge peuvent faire rejeter des jetons comme émis dans le futur », et l’imposition d’un plafond à soixante secondes sert « à empêcher les implémentations de désactiver complètement les vérifications ». Un profil qui anticipe que son exigence sera contournée par une désactivation, et qui préfère borner la tolérance plutôt que la refuser, a compris quelque chose sur le métier.

Deux interdictions contre-intuitives, et leur raison

Section intitulée « Deux interdictions contre-intuitives, et leur raison »

La rotation des jetons de rafraîchissement est prohibée, sauf circonstances extraordinaires. L’exigence surprendra qui a lu ailleurs que cette rotation est une bonne pratique, et la justification mérite d’être comprise : « l’usage de la rotation des jetons de rafraîchissement n’apporte pas de bénéfice de sécurité lorsqu’il est utilisé avec des clients confidentiels et des jetons d’accès contraints par l’expéditeur ». La rotation sert à détecter le vol d’un jeton de rafraîchissement par un client public ; avec un client confidentiel dont les jetons sont déjà liés à une clé, ce vol ne produit rien, et la rotation n’apporte que ses inconvénients : « elle cause une dégradation de l’expérience utilisateur et des problèmes opérationnels dès lors que le client échoue à stocker ou à recevoir le nouveau jeton de rafraîchissement et n’a aucune option pour réessayer ». Le profil l’autorise néanmoins pour des migrations d’infrastructure, à condition que le serveur offre au client « l’option limitée dans le temps de réessayer avec l’ancien jeton en cas d’échec ». C’est un exemple de mesure de sécurité qui cesse d’avoir un sens quand une autre mesure, plus forte, est en place, et que la discipline commande alors de retirer.

Le paramètre d’état n’assure plus la protection contre la falsification de requête inter-sites. Le document le note en passant, et c’est un changement de doctrine : « dans ce document, le paramètre state n’est pas utilisé pour la protection contre la falsification de requête inter-sites, mais peut être utilisé par le client pour l’état applicatif ». La protection est désormais assurée par la combinaison de la requête poussée authentifiée, du défi PKCE lié à l’agent utilisateur et de la vérification de l’émetteur dans la réponse. Un paramètre qui servait deux fonctions n’en sert plus qu’une, ce qui est un progrès de clarté, et un piège pour qui migrerait en conservant ses anciens contrôles sans mettre en place les nouveaux.

Un dernier détail, presque amusant, montre jusqu’où va le souci d’interopérabilité : un ensemble de clés publiques « ne devrait pas contenir plusieurs clés avec le même identifiant de clé », mais le profil décrit tout de même l’algorithme de sélection à appliquer quand c’est le cas, en départageant sur le type, l’usage et l’algorithme. Reconnaître que la règle sera violée et préciser le comportement en cas de violation est une marque de maturité que l’on aimerait voir plus souvent.

Ce que FAPI 2.0 a retiré de FAPI 1.0, et la leçon à en tirer

Section intitulée « Ce que FAPI 2.0 a retiré de FAPI 1.0, et la leçon à en tirer »

Le document compare explicitement les deux générations, et les deux changements principaux sont des suppressions. La requête d’autorisation signée, qui emballait les paramètres dans un jeton, est remplacée par la requête poussée, pour « l’amélioration de la protection d’intégrité et de la compatibilité des requêtes d’autorisation ». Et le mode de réponse signé, qui protégeait l’intégrité de la réponse d’autorisation, devient inutile parce que la réponse « est réduite à ne contenir que le code d’autorisation, rendant obsolète le besoin de protection d’intégrité ». Troisième changement, de méthode : les défenses fondées sur des menaces particulières laissent place à un modèle d’attaquant, à des objectifs de sécurité explicites et aux bonnes pratiques d’OAuth.

Il y a là une leçon qui dépasse largement la banque ouverte. On durcit un protocole en réduisant ce qu’il transporte, pas en ajoutant une couche de signature sur ce qu’il transportait déjà. Si la réponse d’autorisation ne contient plus qu’un code à usage unique, de soixante secondes de durée de vie, lié à un défi PKCE et accompagné de l’identité de l’émetteur, il n’y a plus rien d’intéressant à protéger dedans. C’est exactement le raisonnement qui a conduit à abandonner le flux implicite, et c’est celui qu’il faut appliquer à toute conception : chercher ce qu’on peut retirer avant de chercher ce qu’on peut signer.

Pour une organisation qui n’a pas d’obligation sectorielle, trois usages de ce document sont immédiats. Le lire comme une liste d’interdictions à vérifier sur son fournisseur d’identité existant, ce qui prend une heure et révèle en général deux ou trois options actives qui ne devraient pas l’être. L’adopter comme profil pour les API internes à fort impact, en acceptant ses contraintes sur les clients confidentiels. Et retenir sa règle de composition : un profil interne peut restreindre davantage, jamais assouplir, sous peine de perdre la seule chose qui fait la valeur d’un profil, à savoir la certitude de ce qui se passe quand on ne regarde pas. Le reste des pièges, pour qui part d’un déploiement ordinaire, est dans les pièges OAuth 2.1.

Sans interdiction explicite des options faibles, un déploiement OAuth parfaitement conforme au cadre de base peut accepter l’octroi par mot de passe, émettre des jetons au porteur, authentifier ses clients par secret partagé et renvoyer des jetons en paramètre d’URL, sans qu’aucune de ces décisions ne soit signalée comme un défaut. Sans jeton contraint par l’expéditeur, toute fuite de jeton se transforme en accès. Sans requête d’autorisation poussée et authentifiée, les paramètres restent modifiables dans le navigateur et le client contrefait n’est détecté qu’après l’affichage d’un écran de consentement. Sans vérification de l’identité de l’émetteur dans la réponse d’autorisation, une confusion entre deux serveurs d’autorisation reste exploitable. Sans valeurs chiffrées pour les durées de vie, chaque équipe choisit la sienne, et la plus généreuse fixe le niveau de l’ensemble. Sans règle interdisant d’assouplir un profil, une exigence retirée localement pour faire passer une intégration annule la démonstration qui justifiait tout le reste.

OAuth, le mécanisme qui permet à une application d’accéder à vos données chez un autre service sans connaître votre mot de passe, n’est pas une recette mais une boîte à outils. Il laisse une quantité de choix au développeur, et il suffit qu’un seul de ces choix soit mal fait pour que l’ensemble devienne attaquable.

Un profil de sécurité résout cela en supprimant les choix. Celui que la banque ouverte utilise, appelé FAPI 2.0, dit par exemple : pas de transmission de mot de passe, pas d’applications sans secret, pas de jetons utilisables par quiconque les trouve, deux seules méthodes d’authentification autorisées, pas de jetons dans les adresses internet, une seule forme de réponse. Il remplace aussi les formules vagues par des nombres : un code d’autorisation vit au maximum soixante secondes, une clé fait au minimum tant de bits, trois algorithmes de signature sont permis et pas un quatrième.

Son intérêt principal n’est pas la liste elle-même, mais le fait qu’elle a été vérifiée mathématiquement contre une description précise de ce qu’un attaquant peut faire. D’où une règle inhabituelle : on peut rendre ce profil plus strict, jamais plus souple. Retirer une seule exigence n’enlève pas un peu de sécurité, cela annule la démonstration, et plus personne ne sait ce que garantit le reste.

Deux de ses décisions surprennent. Il interdit de renouveler systématiquement les jetons de longue durée, pratique souvent présentée comme une bonne idée, parce qu’elle n’apporte rien quand les jetons sont déjà rattachés à une clé et qu’elle casse les applications qui perdent le nouveau jeton. Et il accepte une tolérance de dix secondes sur les horloges, non par laxisme, mais parce que l’expérience montre que sans tolérance les développeurs désactivent la vérification entièrement.

La leçon la plus utile du document tient en une phrase : on sécurise un protocole en réduisant ce qu’il transporte, plutôt qu’en ajoutant une signature sur ce qu’il transportait déjà. Quand il ne reste dans le navigateur qu’un numéro valable une minute, il n’y a plus rien à protéger dedans.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.