
- L’écosystème moderne
- Le besoin
- Re.S.T. : REpresentational State Transfer
- Le modèle de maturité de Richardson ou Web Service Maturity Heuristic
- H.A.T.E.O.A.S. & “Resource Linking”
- ReSTful donc Stateless
- Pragmatisme, idéologie et ReSTafarians
- Tips, tricks et bonnes pratiques
- Les “standards” ou presque
- Les outils
- Sécurité des APIs ReST
Voir les Slides
1. L’écosystème moderne
- De moins en moins d’applications monolithiques.
- De plus en plus d’interactions entre les services.
- Les Single Page Applications / Progressive Web Apps etc…
- Micro-Services.
- APIs publiques.
2. Le besoin
Les APIs doivent donc être :
- Flexibles, extensibles et réutilisables.
- Faciles à utiliser et compréhensibles.
- “Separation of Concerns”.
- Compatibles avec le plus de technologies possibles.
- Il faut pouvoir développer des clients et des serveurs légers.
- Et si on réutilisait nos proxy cache (Varnish, Cloudfront etc…).
- Performantes et sécurisées.
3. Re.S.T. : REpresentational State Transfer
3.1. Ce qu’une API ReST n’est pas
- ReST n’est pas un standard mais un “style d’architecture”.
- ReST ne concerne pas uniquement les APIs distantes ou HTTP. Une librairie peut être ReSTful.
3.2. Roy Thomas FIELDING : Papa du ReST
- L’origine des API ReST date de l’année 2000.
3.3. Description du ReST
- Une API ReST est une interface abstraite du modèle de données qu’on appelle ressources.
- On peut distinguer deux principaux types de ressources :
- Les instances (un utilisateur, un produit etc…).
- Les collections (une liste d’instances).
- L’API ReST permet d’ajouter / modifier / supprimer des ressources.
- Contrairement aux APIs SOAP, il faut absolument éviter la logique impérative où on transmet des actions à l’API.
3.4. Les 5 règles et demi de l’API ReST
- Uniforme.
- “Stateless”.
- “Cacheable”.
- Client / Serveur : “Separation of Concerns”.
- “Layered” ou basée sur des “Connectors”.
- Code à la demande (règle optionnelle ou plutôt inadaptée).
- C’est le paramètre “extra” qu’on retrouve dans certaines RFC pour garder un peu de souplesse.
Uniforme
- Chaque ressource est identifiée de façon unique et canonicalisée avec son URL.
- L’interface est uniforme à tous les niveaux. Tous les éléments communiquent en utilisant la même interface.
“Stateless”
- Une API ReST ne doit pas maintenir de session.
- Cela évite entre autres, les problèmes de “load balancing” par exemple.
“Cacheable”
- Il doit être possible de mettre les ressources en cache à tous les niveaux (front, connecteur intermédiaire, back, etc…).
- Il doit être possible d’utiliser les implémentations standards de cache HTTP.
Client / Serveur : “Separation of Concerns”
- L’API ReST n’est pas concernée par l’affichage, les interactions utilisateur et la session.
- Tous ces éléments doivent être gérés par le client (Ex. : application web frontend).
“Layered”
- La présence de “connecteurs” intermédiaires doit être implicite pour le client et le serveur (composant de cache / sécurité etc…).
3.5. Les formats d’échange
- En théorie, le format d’échange est libre.
- En pratique, le format doit être standard et non-linéaire (Hypermedia).
- Plus concrètement, le format le plus utilisé aujourd’hui est le JSON.
- L’univers JavaScript est en expansion permanente.
- Contrairement aux technologies backend habituelles, le nombre de librairies et d’outils utilisés est volontairement restreint pour éviter de surcharger les clients JavaScript.
- On retrouve des outils JSON dans tous les langages.
- JSON est facile et rapide (au sens performance) à manipuler.
3.6. Les méthodes HTTP utilisées
- GET : Récupération d’une ressource ou d’une collection.
- POST : Création d’une ressource.
- PUT : Remplacement d’une ressource ou d’une collection.
- PATCH : Modification d’une ressource ou d’une collection.
- DELETE : Suppression d’une ressource ou d’une collection.
- Plus exactement :
- La méthode POST peut servir à modifier une ressource mais ce n’est pas recommandé.
- La méthode PUT peut servir à créer une ressource si on en connaît l’identifiant par avance par exemple. La seule contrainte sur la méthode PUT est qu’elle doit être idempotente. Le nombre d’exécution d’une même requête ne doit pas impacter le résultat.
4. Le modèle de maturité de Richardson
ou Web Service Maturity Heuristic
https://www.crummy.com/writing/speaking/2008-QCon/act3.html (2008)
*P.O.X. : Plain Old XML
4.1. Level 0 : The Swamp of POX
XML-RPC over HTTP
4.2 – Level 1 : Ressources
L’API respecte le modèle de données et chaque ressource peut être identifiée avec une URL.
POST /blogs/11111/posts POST /blogs/11111/posts/22222/comments
4.3 – Level 2 : HTTP Verbs
Utilisation des méthodes HTTP autres que GET et POST pour signifier l’action souhaitée : PATCH / PUT / DELETE.
… et surtout les “status codes” HTTP pour résumer le résultat de l’opération :
200 : OK
201 : Created
204 : No Content (delete)
400 : Bad Request
401 : Unauthorized
403 : Forbidden
404 : Not Found
409 : Conflict
…
Bien sûr, les erreurs 4xx peuvent contenir un “body” avec des informations additionnelles.
Utilisez le bon vocabulaire et évitez les APIs schtroumpf.
SCHTROUMPF /q?data=select:*:from:carts
4.4 – Level 3 : Hypermedia Controls
Hypermedia est l’une des principales règles de la thèse de Fielding.
L’idée est de retrouver dans les API ReST la même logique Hypermedia qu’en HTML par exemple. Aujourd’hui, cela se résume principalement par la présence de liens dans les ressources permettant de définir la relation avec d’autres ressources.
L’API ReST devient alors “discoverable“.
5 – H.A.T.E.O.A.S. & “Resource Linking”
Le “level 3” du modèle de maturité de Richardson est souvent représenté par l’acronyme H.A.T.E.O.A.S. : Hypermedia As The Engine Of Application State. (Rien de politique donc…)
{ "id": "22222", "href": "https://www.wishtack.com/blogs/11111/posts/22222", "blog": { "href": "https://www.wishtack.com/blogs/11111" }, "comments": { "href": "https://www.wishtack.com/blogs/11111/posts" }, ... }
6. ReSTful donc Stateless
6.1. Stateless ?
Supposons le scénario d’échange suivant. Est-il “stateless” ?
1 - GET /init 2 - GET /select-cart?cartId=123ab 3 - POST /add-product 4 - POST /add-product 5 - POST /update-product-count?productId=12345 6 - GET /cart-summary 7 - POST /pay
6.2. Limites et difficultés du stateful
- L’effet “never-click-back”.
- Problèmes de load balancing.
- Comment paralléliser l’ajout de deux produits dans deux paniers différents ?
- Comment mettre la ressource “/cart-summary” en cache.
- API peu intuitive et peu extensible.
6.3. Exemple Stateless
1.a - POST /carts/123ab/products 1.b - POST /carts/456cb/products 2 - PATCH /carts/456cb/products/33333 3 - POST /carts/123ab/payments 4 - GET /carts/123ab
6.4. Les avantages
- Pas de session à maintenir et donc pas de problème de load balancing.
- Moins de requêtes.
- Il est possible de paralléliser les requêtes.
- “Cacheable”.
- API intuitive et extensible.
- L’API est human readable (pas besoin d’avoir la documentation en permanence sous les yeux).
- L’API est facile à étendre (ajout de propriétés par exemple).
- L’API peut répondre facilement à des besoins qui n’ont pas été anticipé (modification du nombre de produits dans le panier par exemple).
Affordances
En général, on peut comparer les approches stateful et stateless à la métaphore de la destination géographique.
Laquelle de ces deux indications vous semble la plus précise :
- Les coordonnées GPS d’une adresse à Strasbourg.
- Pour aller de Nice à l’adresse à Strasbourg :
- Tournez à droite.
- à 100m à gauche.
- Au rond-point (s’il n’a pas changé depuis), sortez à la 3ème sortie.
- Admirez la vue sur votre gauche
- …
7. Pragmatisme, Idéologie et ReSTafarians
- Comme indiqué précédemment, les principaux objectifs des APIs ReST sont les suivants :
- Généricité.
- Facilité d’implémentation et extensibilité.
- Performance et “scalability”.
- Le ReST n’est un dogme ou une idéologie et il faut donc rester pragmatique tout en préparant l’avenir. Visionnaire et non devin. K.I.S.S. et Agile.
- Evitez donc les forces obscures qui tentent d’appliquer des paradigmes complexes datant de l’âge de l’XML ! (ils sont également soupçonnés d’indenter à 2 espaces et de retirer les “;” en JavaScript…)
- Exemple : https://github.com/kevinswiber/siren.
- Ne soyez pas ReSTafarians ! Le but initial est de répondre aux besoins associés à la User eXperience et la Developer eXperience. On ne cherche pas à être plus ReST que les autres.
8. Tips, tricks et bonnes pratiques
8.1. Nommage
- kebab-case pour les URLs.
- camelCase pour les paramètres en “query string” et pour les “fields” des ressources.
- kebab-case pluriel pour les noms des ressources dans les URLs.
- Je recommande tout de même de convertir les pluriels vers des variables avec un suffixe `List`. Dans les URLs, c’est joli mais dans le code c’est trop subtil et ça provoque facilement des conflits.
- Utilisez des noms explicites respectant la “métaphore” du service.
- Les URLs doivent être construites de la façon suivante :
-
/resources/:resourceId/subresources/:subResourceId /blogs/:blogId/posts/:postId
- Evitez donc les URLs de type :
-
/blogs/:blogId/posts/:postId/summary
- Ce n’est pas du ReSTafaring. De nombreuses librairies sont conçues ainsi. Contourner ces règles vous obligera à modifier, détourner et torturer les librairies et frameworks que vous utilisez.
8.2. Base URL
- La “base URL” est l’URL de la racine de votre API.
- Evitez les URLs complexes :
-
https://www.ibm.com/index.aspx/lastCompanyWeBought/service/rest/
-
- Préférez :
-
https://api.wishtack.com
-
8.3. Media Type
- Le “media type” habituel défini avec le header
Content-Type
estapplication/json
.
- Il est courant de définir un Media Type spécifique pour une API ou éventuellement en fonction du “standard” utilisé.
- Exemple :
application/vnd.github+json
.
- Exemple :
- Les “media type” de type
application/vnd*
ne sont pas standards et peuvent éventuellement poser des problèmes avec certaines librairies ou connecteurs (Ex.: Web Application Firewall).
- Certains s’amusent à retourner un contenu HTML (présentation, documentation, démo etc…) lorsque le client ne présente pas le bon “media type” dans le header
Accept
.- C’est élégant…
- …mais pas pratique du tout ! Qui n’a jamais testé une URL d’API ReST sur son browser ?
- Nous verrons plus tard que pour des raisons sécurité, il est recommandé de rejeter les requêtes ne présentant pas le bon header
Content-Type
.
8.4. Versioning
- Etant donné que les APIs ReST sont conçues pour être utilisées par de multiples sources (clients mobiles / web / desktop / partenaires / public…), elles évoluent souvent à un rythme différent de celui des clients. Il est donc nécessaire des les versioner.
- Deux approches s’offrent alors à nous :
- Versioning par “media type”.
- Versioning par URL.
8.4.1. Versioning par “media type”
Le versioning par “media type” consiste à utiliser le comportement standard des “headers” HTTP Accept
et Content-Type
.
Le client indique alors la version de l’API qu’il supporte dans le header Accept
:
Accept: application/vnd.wishtack.v3+json
L’API retourne alors les données dans la version correspondante avec le bon Media Type dans le header Content-Type
.
- Séduisant mais légèrement complexe à mettre en place.
- Comment faire si la nouvelle version est implémentée dans un langage différent ou encore sur une plateforme différente ?
- Nous serions alors obligé d’utiliser un proxy pour effectuer le “balancing”.
8.4.2. Versioning par URL
- Vu l’obstacle de “balancing” posé par le versioning par “media type”, pourquoi pas utiliser un “balancing” standard en amont… mais lequel ?
- Nous pourrions utiliser le “path” de l’URL et procéder au “balancing” avec des “virtual hosts” sur le nom de domaine mais cela nécessite encore un proxy (mais un peu plus classique cette fois-ci).
https://www.wishtack.com/api/v1
- Qui dit mieux ?
- Yes ! Le DNS !
https://v1.api.wishtack.com
NodeJS hosted on Amazon.
https://v2.api.wishtack.com
Python hosted on Heroku.
https://v3.api.wishtack.com
Python hosted on Heroku sur un monorépo avec l’API V2.
8.5. Propriété “id”
La propriété “id” doit être uniforme.
Les ressources ont un identifiant unique dans une propriété qui est conventionnellement: “id”
8.6. Polymorphisme
Il peut arriver qu’une ressource de type collection contienne plusieurs ressources de types légèrement différents. Par exemple, des produits de type différents : livres et films.
- Tout d’abord, il faut harmoniser le modèle de la ressource au maximum. Par exemple, livres et films ont un prix, il faut que ce soit la même propriété. Même si tel n’est pas le cas dans votre modèle de données (Ex. scraping), créez des “computed fields”. On peut imaginer naïvement un “computed field”
price
qui calcule le prix à partir de la durée du film :).
- Il suffit alors d’ajouter un “field”
type
au modèle de votre ressource (qu’il faudra dûment documenté).
- Cela permet ensuite côté client de “remapper” vers les classes associées.
- L’abus de polymorphisme nuit gravement à la santé de votre API et de ses proches.
[ { "id": "1", "author": {"id": "3"}, "price": {"amount": 10, "currency": "EUR"}, "type": "book", }, { "duration": 5400, "id": "2", "price": {"amount": 6, "currency": "USD"}, "type": "movie" } ]
8.7. Datetime
- Comment échanger les dates et heures avec les APIs ?
- Nous n’utiliserons pas le terme “timestamp” afin éviter les conflits avec le “unix timestamp”.
- Pas de débat à ce sujet, ISO 8601 est là depuis 1997 (chez Wishtack, la moyenne d’age était de 12 ans et on configurait nos IPs pour jouer à Counter-Strike).
- https://www.w3.org/TR/NOTE-datetime
- https://www.iso.org/obp/ui#iso:std:iso:8601:-2:dis:ed-1:v1:en
1997-07-16
1997-07-16T19:20:01.003Z
- Simplifiez la vie de vos clients en convertissant les “datetimes” en UTC.
8.8. “Association Resource”
Supposons la ressource suivante :
/users/123ab/friends [ { "id": ..., "firstName": ..., "type": "user" } ]
- Comment représenter la “datetime” de création du lien entre les utilisateurs ?
Nous pouvons créer une ressource de type collection qui représente ces liens.
/friendships?userId=123ab [ { "id": "FRIENDSHIP_ID_1", "creationDateTime": "2017-01-01T18:16:00.000Z", "friend": { "id": ..., "type": "user" } } ]
…et la ressource d’instance
/friendships/FRIENDSHIP_ID_1
8.9. Avis subjectif sur H.A.T.E.O.A.S. et le Semantic Web
- C’est tout beau…
- …mais comment cela s’intègre dans la pratique ?
- Supposons que nous disposions des objets et méthodes suivantes :
// GET https://v1.api.wishtack.com/users/SOME_USER_ID userStore.getUser({userId: 'SOME_USER_ID'}); // GET https://v1.api.wishtack.com/users/SOME_USER_ID/wishes wishStore.getWishList({userId: 'SOME_USER_ID'});
- Supposons maintenant que l’API réponde avec les données suivantes :
{ "href": "https://api-v1.wishtack.com/users/SOME_USER_ID", "wishes": { "href": "https://api-v1.wishtack.com/users/SOME_USER_ID/wishes" } }
- Comment faire pour réutiliser la méthode
WishStore.getWishList
?
- Faut-il ajouter une méthode
WishStore.getWishListByUrl
?
- Que faire en cas d’incohérence ?
- Les données ne sont pas proprement canonicalisées.
- Les URLs sont dupliquées et occupent une grande partie du contenu. Comment factoriser ?
- Comment récupérer l'”id” si on ne souhaite pas utiliser le “href” comme “id”.
- Une URL est une information qui perd en canonicalisation. Une façon plus canonique de décrire une ressource serait la suivante :
{ "baseUrlList": ["https://api.wishtack.com", "https://api-backup.wishtack.com"], "resourcePath": [ {"id": "USER_ID", "type": "user"}, {"id": "WISH_ID", "type": "wish"} ] }
- Comment basculer automatiquement d’une API principale à une API de backup sans “parser” et reconstruire l’URL ?
- Peut-on faire confiance à une API au point d’utiliser naïvement les URLs qu’elle nous transmet ?
- Il nous faudrait idéalement les informations suivantes :
- “id” de la ressource.
- “type” de la ressource ou encore mieux un référentiel de type. I.A.N.A. ?
- Mapping “type” => informations sur la construction de l’URL (“Base URL” et “Path”).
- Affordances : que puis-je faire avec la ressource ? A quoi correspond-elle ?
C’est tout simplement l’utopie (ou le futur ?) du Web Sémantique. Cela nécessite des standards de canonicalisation des données et une adoption importante.
Pour le moment, on ne trouve que quelques tentatives qui ressemblent plus à de l’HTMLisation du ReST et on y retrouve des noms familiers : Richardson, Amundsen et Foster.
Application-Level Profile Semantics : http://alps.io/spec/drafts/draft-01.html
Intéressant mais assez loin des conventions ReST et du pragmatisme qui nous intéresse.
8.10. Pourquoi appliquer ces bonnes pratiques
Au delà de la généricité et la facilité de compréhension et d’implémentation, l’application de ses bonnes pratiques permet d’implémenter des librairies et des connecteurs génériques sans aucune connaissance de l’API.
- Un cache peut facilement :
- Anticiper la récupération la section suivante d’une ressource paginée.
- Récupérer une ressource depuis sa collection en cache.
- MISS:
/products
- HIT:
/products/1234
- MISS:
- Maintenir la synchronisation entre les données locales et celles de l’API (Progressive Web Apps).
- https://github.com/wishtack/wishtack-steroids/tree/develop/packages/rest-cache
- Un connecteur générique peut gérer les autorisations d’accès aux ressources ou même filtrer les propriétés “readonly” ou “hidden”.
9. Les “standards” ou presque
Nous constatons que dès la conception d’une simple API ReST, de nombreux choix s’offrent à nous et on se retrouve comme des gamins chez “Toys R Us”.
- Pourquoi ces choix sont-ils si importants ?
- Après tout, on pourrait imaginer qu’une bonne documentation suffit.
- Le problème est qu’on rencontrera rapidement des obstacles avec les librairies, frameworks et connecteurs qui se basent fortement sur les conventions ReST.
- Encore mieux que les conventions, il nous faudrait un standard couvrant le maximum parmi les points suivants :
- Format des données.
- Typing des ressources.
- Linking.
- Pagination.
9.1. JSON API
- http://jsonapi.org/
- Créé par le co-fondateur de http://www.tilde.io, une entreprise de conseil (en jsonapi ?)
- C’est une spécification et non un standard.
- Cool :
- Définition d’un format strict mais extensible.
- Standardisation des paramètres de “sorting”, “filtering” et de pagination (l’implémentation reste libre pour la pagination).
- L’idée du “resource linking” avec des “relationships” est intéressante.
- Pas cool :
- Risque de collision entre les “fields” présents dans
attributes
etrelationships
. - Pas de différence entre un lien vers une instance ou une collection.
- Les “one-to-one relationships” sont ambigües et ne respectent pas la convention :
/resources/:resourceId/sub-resources/:subResourceId - Attention, les exemples utilisés dans la spec adoptent des conventions inhabituelles et ne sont pas imposés par la spec.
- L’utilisation des “fields” en kebab-case n’est pas dans le standard.
- La propriété “type” n’est pas forcément au pluriel.
- L’idée des “bulk operations” sur les “relationships” est très intéressante mais malheureusement pas appliquée aux ressources.
- De nombreuses implémentations mais la plupart ne sont plus maintenues depuis des mois voire des années.
- Risque de collision entre les “fields” présents dans
9.2. H.A.L.
- Hypertext Application Language.
- Créé par le fondateur de http://stateless.co/, une entreprise de conseil.
- Ce n’est pas un standard non plus.
{ "_links": { "self": { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true } }, "_embedded": { "orders": [{ "_links": { "self": { "href": "/orders/123" }, "basket": { "href": "/baskets/98712" }, "customer": { "href": "/customers/7809" } }, "total": 30.00, "currency": "USD", "status": "shipped", },{ ... }] }, "currentlyProcessing": 14, "shippedToday": 20 }
- Cool :
- Simple, clair et facile à implémenter.
- Les “templated links” sont très prometteurs et permettent un découplage entre le code client et l’API.
- Les “curies” permettent de facilement lier les ressources à leur documentation et pourquoi pas un schéma (mais ce n’est pas défini par H.A.L.).
- Pas cool :
- Comme son nom l’indique, H.A.L. se focalise uniquement sur le “linking”. Le périmètre est donc très limité.
- La propriété “_embedded” manque d’intérêt et peut provoquer des conflits entre les propriétés de la ressource et les propriétés “_embedded”.
- De nombreuses implémentations mais la plupart ne sont plus maintenues depuis des mois voire des années.
9.3. JSON LD
- A JSON-based Serialization for Linked Data.
- https://www.w3.org/TR/json-ld/
- Créé par de nombreux auteurs fortement associés à l’univers du Web sémantique, RDF etc…
- Ce n’est pas un standard mais une recommandation W3C.
- Exemple : http://json-ld.org/playground/
- Cool :
- Utilise les “contextes” de shema.org.
- Possibilité de créer des “contextes” personnalisés.
- Pas cool :
- Hérite de la culture XML / RDF.
- Quelques implémentations mais la plupart ne sont plus maintenues depuis des mois voire des années.
9.4. Les autres initiatives
- Collection+json https://github.com/collection-json/spec
- Hydra http://www.markus-lanthaler.com/hydra/
- Vocabulaire ReST pour JSON LD.
- Permet d’ajouter la notion d’affordances.
9.5. So what?
Ce qu’il faut retenir :
- JSON API, H.A.L. et JSON-LD se battent sur des terrains différents qui se croisent à certains endroits.
- JSON-LD reçoit le plus de soutien de la communauté Hypermedia.
- Tant qu’aucun standard ne s’impose, il faut essayer de prendre le meilleur de chaque monde en fonction de votre besoin.
- N’oubliez pas qu’il faut parser, sérialiser et “linker” les ressources dans les langages que vous utilisez vous et vos partenaires. Plus le format sera complexe, plus vous impacterez négativement l’adoption de votre API.
10. Outils
10.1. Swagger
Swagger est un framework qui vous permet de définir et documenter vos APIs ReST.
10.2. Postman
Postman est un client ReST très pratique pour analyser, expérimenter et debug vos APIs ReST.
Pensez à essayer l’extension Chrome qui permet d’analyser le trafic et rejouer les requêtes.
10.3. Sandbox
Le meilleur terrain de jeu pour s’amuser avec les APIs ReST.
10.4. JSON Generator
JSON Generator vous permet de générer facilement des données JSON pour vos tests unitaires par exemple.
http://www.json-generator.com/
11. Sécurité des APIs ReST
Les architectures modernes et distribuées dont les APIs ReST font partie nous exposent à de nouveaux risques.
Il en est de même pour certains mécanismes d’authentification modernes dont les spécifications ne sont malheureusement pas assez strictes.
11.1. Authentification et “session management”
11.1.1. De quoi avons nous besoin ?
- Session management ?
- Nope ! L’API ReST doit être Stateless !
- Si des informations liées à la session doivent être maintenues, celles-ci doivent être gérées par le client.
- Les données persistées par l’API ReST sont associées à des ressources.
- Rien n’interdit l’expiration d’une ressource :
GET /carts/1234 => 404 Not Found
- Ne schtroumpfez pas
SMURF /smurf-api/sessions/current
- Authentification
- Idéalement, il nous faudrait un mécanisme d’authentification même si les données de l’API sont publiques.
- Identification
- Si nécessaire. L’authentification et l’identification sont des notions distinctes.
- Il est possible d’authentifier un utilisateur sans l’identifier.
- Il est également possible d’identifier un utilisateur sans l’authentifier mais nous n’aurions aucune garantie de l’identité.
- Logout et révocation
- Le “logout” peut provenir d’une autre source que l’utilisateur final.
- Les “tokens” d’authentification ne doivent pas être transmis dans l’URL.
GET /users/123?token=asdf....
- L’authentification “basic-auth” ne doit pas être utilisée.
- Les “tokens” doivent être transmis dans le “header”
Authorization
Authorization: Bearer xxxxxx, Extra yyyyy
11.1.2. Mécanismes d’authentification
- Nous parcourerons plus tard les différents mécanismes d’authentification envisageables.
- Globalement (modulo quelques étapes), la plupart des mécanismes d’authentification fonctionnent ainsi :
- Le service d’authentification fournit un “token” unique au client.
- Le client transmet ce “token” aux APIs ReST du fournisseur de service.
- Le fournisseur de service déduit les autorisations d’accès en fonction de ce “token”.
11.1.3. “Session management” côté client
- Le cas le plus complexe est celui où le client est un “browser”.
- Si vous souhaitez persister des données dans le “browser” afin que l’utilisateur puisse retrouver le même contexte en changeant de fenêtre ou après un “refresh” :
- Evitez absolument l’utilisation des “cookies” ne serait-ce que pour les raisons suivantes :
- Ce n’est pas leur rôle.
- Vous ne voulez pas envoyer toutes ces données au backend à chaque requête.
- Cookies are EVIL !
- L'”indexedDB” est là pour ça mais malheureusement, il n’est pas encore supporté globalement mais le “localStorage” reste une solution de backup.
- Problème 😦
- L'”indexedDB” et le “localStorage” n’ont pas de notion d’expiration sauf sur Firefox : https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open.
- Jetez un coup d’oeil au contenu de vos storages après “logout” ou fermeture du “browser”, vous serez surpris de découvrir ce qu’on y retrouve.
- “Secure Storage”
- En attendant une solution “in-the-browser”, il est recommandé de chiffrer les données stockées localement avec une clé temporaire et unique pour chaque client transmise par l’API ReST.
- https://github.com/jas-/crypt.io
- Evitez absolument l’utilisation des “cookies” ne serait-ce que pour les raisons suivantes :
11.2. Autorisation et gestion des permissions
- En fonction des “credentials” du client, les autorisations et permissions d’accès sur une ressource peuvent varier.
- Une même ressource peut donc retourner des informations différentes en fonction du client. Cela ne transgresse pas les principes ReST.
- Par exemple, pour la même ressource “user”, un rôle “owner” et un rôle “friend” n’auront pas accès aux mêmes opérations et propriétés :
Avec le rôle “owner”
-
GET /users/123 (with owner role) { "id": "123", "firstName": "John", "lastName": "DOE", "address": { "street": "...", ... } }
Avec le rôle “friend”
-
GET /users/123 (with friend role) { "id": "123", "firstName": "John", "lastName": "DOE" }
- Il y a trois niveaux d’autorisation :
- Niveau ressource : autorisation d’accès à la ressource.
- Niveau verbe : méthodes autorisées sur la ressource (create / read / update / delete).
- Niveau propriété : gestion de l’autorisation par propriété (read / write / mask / restricted values per role).
Ce dernier niveau est malheureusement souvent omis par la plupart des implémentations et frameworks d’API ReST.
- Par exemple, la ressource “post” d’un blog peut avoir une propriété “state” pouvant prendre les valeurs suivantes : “draft”, “private”, “published”.
- Les utilisateurs avec le rôle “administrator” peuvent modifier cette propriété.
- En revanche, les utilisateurs avec le rôle “editor” peuvent modifier toutes les autres propriétés sauf celle-ci.
- Peu importe l’implémentation, les permissions doivent être faites à base de “whitelist“.
- Pour respecter la “separation of concerns”, améliorer la “scalability” et faciliter l’implémentation et la compréhension de l’API ReST, l’implémentation des permissions doit se faire sur un connecteur dédié.
On sépare ainsi l’implémentation fonctionnelle de l’implémentation des permissions.
Pour commencer, cette implémentation peut se faire dans un middleware du framework qui plus tard pourra être migré vers un micro-service dédié.
- Ce connecteur est similaire aux A.C.L. (Access Control List) que l’on retrouve dans les filesystems ou sur les firewalls.
- Chez Wishtack, nous utilisons un middleware Python
- Quelques liens pour les chanceux qui implémentent leurs APIs en Python.
http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions
http://django-tastypie.readthedocs.io/en/latest/authorization.html - Une alternative NodeJS https://github.com/nyambati/express-acl mais pas aussi simple à surcharger que django-rest-framework pour le contrôle des propriétés.
- Quelques liens pour les chanceux qui implémentent leurs APIs en Python.
11.3. Validation : “Canonicalization”, “Escaping” & “Sanitization”
- Toutes les propriétés échangées avec l’API ReST doivent être validées par l’API.
- La validation doit également être implémentée côté client pour éviter les aller-retours inutiles.
11.3.1. “Canonicalization”
- L’API ReST doit convertir les données reçues vers leur forme canonique ou les rejeter.
-
{ "firstName": "joHn", "lastName": " DoE", "url": "myWebsite.com" }
- Doit être transformé ou encore mieux, simplement rejeté.
{ "firstName": "john", "lastName": "doe", "url": "https://mywebsite.com" }
11.3.2. “Escaping”
- Ce n’est pas à l’API ReST de gérer l’escaping du contenu.
- Par exemple, sur un blog, le commentaire suivant est cohérent :
<img src="not-found" />
- C’est au client de gérer l’escaping est d’éviter les attaques de type XSS.
11.3.3. “Sanitization”
- La “sanitization” est un jeu dangereux qui consiste à retirer le contenu potentiellement malicieux.
- Pour l’exemple précédent, cela consisterait à retirer la partie
onerror
<img src="not-found" />
- Mais encore une fois, il s’agit d’une problématique client.
- La difficulté est qu’il est toujours possible de trouver des techniques pour “bypass” la “sanitization”.
- Certains en ont fait leur métier 🙂
http://n0p.net/penguicon/php_app_sec/mirror/xss.html
11.4. Cookies are EVIL
- Les “cookies” nous exposent à des vulnérabilités de type C.S.R.F (Cross Site Request Forgery) que nous aborderons plus tard.
- Les clients ne sont pas toujours des “browsers” (Mobile, Desktop, un micro-service, un partenaire).
- Les “cookies” seront envoyés à chaque requête.
- Il y a un couplage fort entre le “cookie” et la session. En revanche, en utilisant le “header”
Authorization
nous pouvons envoyer des requêtes avec des “tokens” différents à la même API.
Exemple : une session avec plusieurs comptes utilisateurs sans établir aucun lien entre les utilisateurs. C’est le cas sur les services google / twitter / facebook etc… ou encore la fonctionnalité “voir ma page facebook en tant que X”.
11.5. C.O.R.S.
- Cross-Origin Resource Sharing. https://www.w3.org/TR/cors/
- Origin : https://tools.ietf.org/html/rfc6454
- L’ “origin” est composé des éléments suivants :
- Scheme : http / https / …
- FQDN : http://www.attacker.com / api.target.com / …
- Port : 80 / 443 / 8000
- L’ “origin” est composé des éléments suivants :
- Supposons que nous avons deux “origins” :
https://attacker.com
ethttps://target.com
.
- Par défaut, si l’application
https://attacker.com
émet une requêteGET
depuis le browser (en JavaScript) vers l’ “origin”https://target.com
:- Celle-ci ne transmettra aucun des cookies des deux “origins”.
- Le browser analysera ensuite certains “headers” C.O.R.S. (que nous verrons plus tard) mais en leur absence, il ne transmettra pas la réponse à l’application.
Error: No 'Access-Control-Allow-Origin' header is present on the requested resource
- S’il s’agit d’une requête autre que
GET
(ou similaireHEAD
…) le “browser” envoie une “preflight request” de typeOPTIONS
pour vérifier si la requête est autorisée en fonction de l’ “origin” et de la méthode utilisée.
- Ces mécanismes ont été mis en place pour éviter les attaques de type C.S.R.F (Cross Site Request Forgery).
- Malheureusement, le premier résultat sur lequel on tombe en recherchant le message d’erreur est le suivant :
http://stackoverflow.com/questions/20035101/no-access-control-allow-origin-header-is-present-on-the-requested-resource
- On y trouve les propositions suivantes :
- “The easy way is to just add the extension in google chrome to allow access using CORS.”
https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?hl=en-US chrome.exe --user-data-dir="C:/Chrome dev session" --disable-web-security
- “It’s very simple to solve if you are using PHP. Just add the following script in the beginning of your PHP page which handles the request:”
<?php header('Access-Control-Allow-Origin: *'); ?>
- “The easy way is to just add the extension in google chrome to allow access using CORS.”
- …rien n’interdit l’incitation à la vulnérabilisation du web.
- Fiouf, après la mise en place du “header”
Access-Control-Allow-Origin: *
, la requête émise depuis l’ “origin”https://attacker.com
vershttps://target.com
ne contient pas de cookies.
- Il faut activer l’option
withCredentials
de l’objet XHR ou la fonctionfetch
.
- La requête est alors envoyée avec les cookies mais encore une fois les spécifications C.O.R.S. sont rigoureuses et il n’est pas possible de récupérer le contenu de la réponse si le “header”
Access-Control-Allow-Origin
vaut*
.
- Pour pouvoir transmettre des cookies et récupérer la réponse, il faut configurer le “header”
Access-Control-Allow-Credentials
mais encore une fois, heureusement que ce n’est pas suffisant. Cette fonctionnalité ne peut pas être activée siAccess-Control-Allow-Origin
vaut*
.
- Il faut donc définir une “whitelist” d’ “origins” mais malgré tous ces obstacles volontaires, certains vont jusqu’au bout…
http://stackoverflow.com/questions/26411480/angularjs-a-wildcard-cannot-be-used-in-the-access-control-allow-origin-he
Please don’t!!!
- Après cette ultime étape, n’importe quelle application depuis n’importe quel “origin” peut communiquer librement avec votre API en utilisant les “credentials” présents dans les “cookies” de l’utilisateur actuellement authentifié.
- Par précaution, même en l’absence de “cookies”, il vaut mieux éviter d’utiliser la valeur
*
pour le “header”Access-Control-Allow-Origin
.
- Il est préférable d’implémenter une logique de “whitelist” sur l’API qui vérifie le contenu du “header”
Origin
de la requête et le renvoie dans le “header”Access-Control-Allow-Origin
de la réponse en cas d’autorisation réussie.
- La vérification de la “whitelist” doit être stricte ! Il ne suffit pas de vérifier le FQDN.
- Pensez à implémenter une règle sur votre W.A.F. (Web Application Firewall), middlewares ou monitoring sécurité pour détecter et bloquer les réponses HTTP contenant le “header”
Access-Control-Allow-Credentials
.
- Attention ! Les certificats clients et l’authentification de type “basic auth” sont également considérés comme des “credentials” et on rencontre les mêmes problèmes qu’avec les “cookies”.
11.6. C.S.R.F.
- “Cross Site Request Forgery” est une attaque “in-the-browser” dont le scénario est le suivant :
- Un utilisateur “victime” doit être authentifié sur l’application “vulnérable”.
- L'”attaquant” doit réussir à faire visiter une application qu’il contrôle (entièrement ou partiellement) par la “victime”.
- Lors de la visite de la “victime”, l'”attaquant” déclenche une opération sur l’application “vulnérable” en utilisant implicitement les “credentials” de la “victime”.
- On suppose qu’une requête de type “GET” ne peut pas déclencher d’opération “sensible” car autrement il suffirait de rediriger l’utilisateur vers l’URL en question.
- Si les règles C.O.R.S. sont désactivées par l’un des moyens décrits dans le chapitre “C.O.R.S.”, l'”attaquant” peut simplement déclencher une requête “POST” de son choix à destination de l’application “vulnérable” en utilisant les “credentials” de la “victime”.
- Si la “whitelist” d'”origins” n’effectue pas une vérification rigoureuse, l'”attaquant” pourrait éventuellement contrôler le domaine “http” de l’application vulnérable en ciblant le domaine “https” de l’application vulnérable.
11.7. C.S.R.F. & Content-Type
- L’une des erreurs classiques est d’accepter des “media types” autres que application/*json (“header”
Content-Type
).
- Qu’est ce que cela implique ?
- Sans aucune autre erreur de configuration C.O.R.S., l’acceptation du “mime type”
application/x-www-form-urlencoded
permet à l’attaquant de créer un formulaire et de déclencher une simple requête POST.document.querySelector('form').submit()
- Dans ce cas, la plupart des frameworks (Ex. : expressjs) récupèrent un objet :
{ email: 'pwned.by@attacker.io', grants: 'all' }
- Il ne faut donc activer que le parser JSON.
- …mais supposons qu’il soit activé sur tous les “media types” et plus particulièrement
text/plain
pour simplifier la vie des développeurs des clients. - L’attaquant n’a plus qu’à adapter légèrement le formulaire précédent :
<form method="POST" action="https://app.vulnerable.com/api/products/0/admins" enctype="text/plain"> <input name='{"email": "pwned.by@attacker.io", "grants": "all", "extra": "' value='"}'> </form>
- Cela va alors envoyer le “body” suivant :
{"email": "pwned.by@attacker.io", "grants": "all", "extra": "="}
- L’API va alors “parser” le contenu suivant :
{ email: 'pwned.by@attacker.io', grants: 'all', extrat: '=' }
- La vérification du “media type” des requêtes doit donc être rigoureuse.
C.S.R.F. “Mitigation”
- En attendant l’abandon des “credentials” de type “cookie”, “basic auth” et certificat client, une solution de mitigation des attaques de type C.S.R.F. est de positionner un “cookie” (non http-only) contenant un “token” aléatoire et imprévisible.
- L’application client (JavaScript) doit alors envoyer la valeur de ce “token” dans le “header”
Authorization
(Ex. :Authorization: Bearer ..., Csrf: ...
) à chaque requête.
- L’API n’a plus qu’à comparer les deux valeurs présentes dans le “cookie” et dans le “header”.
11.8. C.S.R.F. & “Resource Linking”
- Nous avons évoqué précédemment le problème de confiance lié au “resource linking” en général.
- Un client (browser ou autre) communique généralement avec plusieurs APIs.
- Une réponse malicieuse ou simplement maladroite d’une API pourrait pousser le client à forger une requête vers une autre API en envoyer le token d’authentification ou encore d’autres informations confidentielles.
{ "firstName": "Foo", "address": { "href": "https://api.attacker.com/" } }
- Il est possible de se protéger partiellement avec des règles C.S.P. (Content Security Policy)
connect-src
https://w3c.github.io/webappsec-csp/
- Autrement, il est recommandé d’implémenter ou d’utiliser une librairie HTTP robuste avec des “whitelists” strictes ou le rejet d’URLs absolues bien qu’une URL relative puisse être également malicieuse.
11.9. OAuth 2
- Comme son nom ne l’indique pas, “OAuth 2” est un protocole d’autorisation et non d’authentification.
- OAuth 2 est l’un des standards (stade IETF: Proposed Standard) d’autorisation les plus communs et répandus du Web.
11.9.1. OAuth 2 Roles
- OAuth 2 définit 4 rôles :
- Resource Owner : Une entité disposant de la légitimité et du pouvoir décisionnel lui permettant d’autoriser l’accès à une ou plusieurs ressources protégées.
- Ex.: L’utilisateur des services google qui souhaite autoriser l’accès à son agenda à une application d’agrégation d’agendas.
- Resource Server : Ce service détient les ressources protégées. Il est capable de répondre aux requêtes d’accès à ces ressources en fonction des “access tokens” présentés.
- Ex. : Google Calendar.
- Client : Une application émettant des requêtes à destination du “Resource Server” au nom du “Resource Owner” et avec son autorisation.
Le “Client” peut être entièrement frontend (Web / Progressive Web App / Mobile Web App / Desktop etc…) ou backend (Serveur / Minitel etc…).
- Resource Owner : Une entité disposant de la légitimité et du pouvoir décisionnel lui permettant d’autoriser l’accès à une ou plusieurs ressources protégées.
- Ex. : L’application d’agrégation d’agendas.
- Nous distinguerons deux types de “Clients”
- Confidential (Ex. : Backend)
- Public (Ex. : Frontend, Single Page App, Progressive Web App, Mobile, Desktop, Appliance…)
- Authorization Server : Un serveur qui fournit des “access tokens” après authentification du “Resource Owner” et obtention des autorisations.
- Ex. : Service d’authentification et d’autorisation google. Google accounts.
11.9.2. OAuth 2 Flows
11.9.2.1. OAuth 2 Grant Type: Authorization Code
OAuth 2 propose 4 “flows” différents dont le plus commun est le suivant :
- 1 – Le “Client” redirige le “Resource Owner” vers l'”Authorization Server” :
https://auth.wishtack.com/v1/oauth/authorize? response_type=code &client_id=CLIENT_ID &redirect_uri=https://www.wishtack.com/oauth // optional &scope=read &state=... // state is recommended thus optional 😢
- 2 – Le “Resource Owner” confirme ou rejette les autorisations d’accès demandées sur l’interface proposée par l'”Authorization Server”.
- 3 – Le “Client” reçoit l'”Authorization Code” par redirection :
https://www.wishtack.com/oauth?code=AUTHORIZATION_CODE&state=...
- 4 – Le “Client” peut alors échanger l'”Authorization Code” contre un “Access Token” auprès de l’API OAuth 2 de l'”Authorization Server”.
POST https://auth.wishtack.com/v1/oauth/token client_id=CLIENT_ID &client_secret=CLIENT_SECRET &grant_type=authorization_code &code=AUTHORIZATION_CODE &redirect_uri=https://www.wishtack.com/oauth
5 – En cas de succès, le “Client” reçoit alors l'”Access Token” et un “Refresh Token” optionnel :
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"wishtack_user_data":{
"first_name":"John",
"last_name": "DOE",
"email":"j.doe@ibm.com",
"is_cool": "definitely not!"
}
}
- En cas d’expiration de l'”Access Token” et si le “Client” a reçu un “Refresh Token”, le “Client” peut renouveler sa demande avec le “Refresh Token” et obtenir de nouveaux “Access Token” et “Refresh Token”.
- L'”authorization code” est un code à usage unique dont la durée de vie doit être très courte (moins de 10 minutes).
11.9.2.2. OAuth 2 Grant Type: Implicit
- L'”Implicit Flow” est un mode dégradé de l'”Authorization Code Flow”.
- Il est inévitable quand le “Client” est public (non confidentiel).
- 1 – Le “Client” redirige le “Resource Owner” vers l'”Authorization Server” :
https://auth.wishtack.com/v1/oauth/authorize? response_type=token &client_id=CLIENT_ID &redirect_uri=CALLBACK_URL &scope=read &state=...
- 2 – Le “Resource Owner” confirme ou rejette les autorisations d’accès demandées sur l’interface proposée par l'”Authorization Server”.
- 3 – L'”Authorization Server” redirige le “Resource Owner” vers le “Client” qui reçoit alors directement l'”Access Token” dans le fragment de l’URL.
https://www.wishtack.com/callback# access_token=ACCESS_TOKEN &token_type=bearer &scope=... &state=...
- 4 – Le “User-Agent” suit la redirection mais le fragment ne quitte pas le “device”.
- 5 – Le “User-Agent” exécute alors le code permettant d’extraire l'”Access Token” du fragment.
- 6 – L'”Access Token” est transmis au “Client”.
- Certains “User-Agents” coquins ont tendance à perdre les fragments
https://bugs.webkit.org/show_bug.cgi?id=24175
- Le “Resource Owner” détient l'”Access Token” et peut donc court-circuiter le “Client” pour communiquer directement avec le “Resource Server”.
- Autrement dit, en cas de “man-in-the-middle”, un attaquant peut demander des autorisations au nom du “Client” et utiliser librement l'”Access Token” pour communiquer avec le “Resource Server”.
- C’est pour ces raisons entre autres qu’il est recommandé d’utiliser le flow “authorization code”.
11.9.2.3. OAuth 2 Grant Type: Resource Owner Password Credentials
- Dans les rares cas où le lien de confiance entre le “Resource Owner” et le “Client” est très fort, le “Resource Owner” peut transmettre ses “credentials” directement au “Client”.
- Le “Client” transmet alors les “credentials” à l'”Authorization Server” pour obtenir un “Access Token”.
POST https://auth.wishtack.com/v1/oauth/token grant_type=password &username=... &password=... &client_id=CLIENT_ID // If client is confidential &client_secret=CLIENT_SECRET // If client is confidential
- Ce “flow” est rarement implémenté pour les raisons suivantes :
- Le “Resource Owner” ne peut pas valider les autorisations demandées.
- Il ne permet pas d’autres modes d’authentification que le password.
- Les “Clients” ne sont pas conçus pour véhiculer des credentials.
- Ce “flow” est un peu le “default” d’un “switch/case” qu’on trouve souvent dans les specs afin d’augmenter les chances d’adoption.
- “OpenID Connect” n’interdit pas l’utilisation de ce “flow” afin d’être compatible avec “OAuth 2” mais il est complètement occulté de la spec.
11.9.2.4. OAuth 2 Grant Type: Client Credentials
- Le “Client” peut demander un “Access Token” à l'”Authorization Server” afin d’accéder à ses propres données.
POST https://auth.wishtack.com/v1/oauth/token grant_type=client_credentials &client_id=CLIENT_ID &client_secret=CLIENT_SECRET
11.9.3 OAuth 2 Registration
- Avant qu’un “Client” ne puisse communiquer avec un “Authorization Server”, il faut procéder à une inscription.
- Cette inscription peut se faire de différentes manières (hors spec OAuth 2).
- Par API.
- Via une interface applicative.
- Offline.
- Un mix.
- L'”Authorization Server” doit à minima obtenir les informations suivantes :
- Client type : “confidential” ou “public”.
- Redirect URIs : La ou les URIs vers lesquelles l'”Authorization Server” redirigera le “Resource Owner” après validation des autorisations.
- Si le “client type” est “public”, l'”Authorization Server” ne fournira pas de “Client Secret” est peut restreindre l’accès aux ressources.
11.9.4. OAuth 2 Risques & Recommandations
- TLS EVERYWHERE! OAuth 2 n’oblige malheureusement pas l’utilisation du TLS pour les “Redirect URI” mais le recommande fortement.
- L'”Authorization Server” doit vérifier que le “Client” possède bien le nom de domaine associé à la “Redirect URI”. (Ex. : Vérification de la capacité du client à ajouter une entrée DNS TXT proposée aléatoirement par l'”Authorization Server”)
- L'”Authorization Server” devrait permettre aux “Clients” de renouveler rapidement le “Client Secret”.
- L'”Authorization Server” devrait obliger les “Clients” à renouveler régulièrement le “Client Secret”.
- Le “Client Secret” est souvent stocké dans une variable d’environnement sur le serveur d’application.
- Une erreur de configuration suffit pour compromettre tout le système.
- L'”Authorization Server” peut permettre de modifier le “Redirect URI” simplement en présentant le “Client ID” et le “Client Secret”.
- Les “Redirect URIs” sont des “absolute URIs” (scheme, fqdn, port, path) et ne doivent pas contenir de “fragment” (#…).
- L'”Authorization Server” doit vérifier les “Redirect URIs” avec une égalité stricte.
https://www.wishtack.com/oauth?source=test
est strictement différente dehttps://www.wishtack.com/oauth
.
11.9.5. OAuth 2 Substitution Attack
11.9.5.1. Description de l’attaque
- Cette attaque suppose que l’attaquant et la victime sont des “Resource Owners” inscrits auprès du même “Authorization Server”.
- L’attaquant initie un flow de type “authorization code” ou “implicit”.
- L’attaquant interrompt le scénario à l’étape 3 quand il obtient l'”authorization code” ou l'”access token”.
- L’attaquant incite la victime à suivre un lien pointant vers l’URL contenant l'”authorization code” ou l'”access token” obtenu à l’étape précédente (social engineering ou application malicieuse).
- La victime suit l’URL vers le “Client” qui interagit alors avec les ressources de l’attaquant.
11.9.5.2. Quelques exemples de scénarios
- Banque
- L’attaquant et la victime sont clients d’une même banque.
- La victime se retrouve alors sur l’application de la banque avec les données de l’attaquant.
- En pensant télécharger son propre RIB, la victime récupère le RIB de l’attaquant.
- Messagerie
- L’attaquant usurpe l’identité de la victime en créant un faux compte sur Facebook.
- L’attaquant ajoute des “amis” de la victime.
- L’attaquant s’inscrit sur une application de messagerie utilisant le service OAuth 2 de Facebook.
- La victime se retrouve sur l’application de messagerie avec le compte de l’attaquant.
- L’attaquant se connecte à son tour sur l’application de messagerie pour récupérer les correspondances de la victime.
11.9.5.3. Origine de la vulnérabilité et solution
- Cette vulnérabilité existe car OAuth 2 n’impose aucun lien entre l’étape 1 (demande de l'”authorization code” ou “token”) et l’étape 3 (récupération de l'”authorization code” ou “token”).
- Heureusement, il existe un paramètre optionnel “state”, initialement prévu pour que le “Client” puisse retrouver son état initial après l’autorisation.
- Ce paramètre est désormais détourné de son objectif initial. Il permet de lutter contre cette attaque en vérifiant que le “resource owner” autorisé est bien celui à l’origine de la demande.
- Cela s’implémente le plus souvent de la façon suivante :
- Le “Client” génère un “nonce” imprédictible et unique à chaque demande d’autorisation.
- Le “Client” le positionne par exemple dans un “cookie” et dans le paramètre “state” avant de rediriger le “Resource Owner” vers l'”Authorization Server”.
- L'”Authorization Server” redirige alors le “Resource Owner” vers le “Client” en transmettant le “state” à l’identique.
- Le “Client” vérifie que le “state” correspond au “nonce” dans le “cookie”.
- Malheureusement, il s’agit d’une vulnérabilité conceptuelle dans le standard OAuth 2 et qui se joue à un mot près.
- Le paramètre “state” est donc “RECOMMENDED” au lieu d’être “REQUIRED” laissant ainsi le choix au “Client” de rester vulnérable à cette attaque.
- Si l'”Authentication Server” rend ce paramètre obligatoire, il n’est alors plus conforme au standard.
- “OpenID Connect” ajoute une notion de “nonce” plus explicite mais pour rester compatible avec OAuth 2, ce paramètre est également optionnel 😭.
11.9.5.4. Solution et contournements
- La solution la plus rigoureuse est de rendre le paramètre “state” obligatoire mais bien sûr, sans vérification côté “Client”, ce paramètre est inutile. Par contre, on sort alors du standard.
- L'”Authorization Server” peut réduire le périmètre d’autorisation en l’absence du paramètre “state”.
- C’est l’une des raisons pour lesquelles il est nécessaire de réduire la durée de vie de l'”authorization code” au minimum. En revanche, si le client utilise le flow “implicit”, on ne peut pas réduire la durée de vie de l'”access token” à quelques minutes.
11.10. J.O.S.E. (Javascript Object Signing and Encryption)
- JOSE est un framework destiné à fournir une méthode pour transférer de manière sécurisée des “claims” (informations d’autorisations par exemple) entre différentes entités.
https://datatracker.ietf.org/wg/jose/charter/
- JOSE définit principalement les 4 éléments suivant :
- J.W.K. (JSON Web Key)
Définit le format de la représentation JSON d’une clé cryptographique symétrique ou asymétrique. - J.W.S. (JSON Web Signature)
Définit la représentation d’un contenu signé. - J.W.E. (JSON Web Encryption)
Définit la représentation d’un contenu chiffré. - J.W.T. (JSON Web Token)
Définit une représentation compact et URL-safe d’un “token” (optionnellement signé ou chiffré ou signé puis chiffré) ainsi que les “claims” standardisés et enregistrés auprès de l’IANA.
- J.W.K. (JSON Web Key)
- JOSE ne définit pas de mécanisme d’authentification ou d’autorisation.
11.10.1. JWK
- Clé symétrique destinée à du chiffrement AES256 avec validation d’un hash HMAC SHA512.
{ "kty":"oct", // Key type : Octet Sequence. "alg":"A256CBC-HS512", // Algorithm intended for this key. "k":"GawgguFyGrWKav7AX4VKUg" // Key. "kid": "0" // Key Id. }
- Clé publique asymétrique destinée à la signature avec sa chaîne de certification X509.
{ "kty":"RSA", // Key type: RSA. "alg": "RS512", // RSA SHA512. "use":"sig", // signature. "kid":"1b94c", // Key Id. "n":"vrjOfz9Ccdgx5nQudyhdoR17V...", "e":"AQAB", "x5c": ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCS...A6SdS4xSvdXK3IVfOWA=="] }
- Clé privée asymétrique destinée au chiffrement.
{ "kty":"RSA", "kid":"3j4h", "use":"enc", "n":"t6Q8PWSi1dkJj9hTP8hNYF...PFGGcG1qs2Wz-Q", "e":"AQAB", "d":"GRtbIQmhOZtyszfgKdg4...SdSgqcN96X52esAQ", "p":"2rnSOV4hKSN8sS4Cgc...Ngqh56HDnETTQhH3rCT5T3yJws", "q":"1u_RiFDP7LBYh3N4GXL...TB7LbAHRK9GqocDE5B0f808I4s", "dp":"KkMTWqBUefVwZ2_Dbj1...2pYhEAeYrhttWtxVqLCRViD6c", "dq":"AvfS0-gRxvn0bwJoMSnF...Y63TmmEAu_lRFCOJ3xDea-ots", "qi":"lSQi-w9CpyUReMErP1RsBL...2lNx_76aBZoOUu9HCJ-UsfSOI8" }
11.10.2. JWS : Signature Asymétrique ou “Signature” Symétrique
- Représentation d’un contenu signé.
{ "payload": "eyJpc3MiOiJqb2...kjp0cnVlfQ", "signatures": [ { "protected":"eyJhbGciOiJSUzI1NiJ9", "header": {"kid":"123"}, "signature": "cC4hiUPoj9E...cN_IoypGlUPQGe77Rw" }, { "protected":"eyJhbGciOiJFUzI1NiJ9", "header": {"kid":"456"}, "signature": "DtEhU3ljbEg8L38VWA...Kg6NU1Q" } ] }
- Il est possible d’utiliser des clés symétriques pour authentifier un message avec HMAC. Il s’agit alors d’un message authentication code et non d’une signature.
11.10.3. JWE : Chiffrement Asymétrique ou Symétrique
- Représentation d’un contenu chiffré.
{ // Integrity protected header but not encrypted! "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0", "unprotected": {"jku":"https://server.example.com/keys.jwks"}, "recipients":[ { // Key and Alg hints. "header": {"alg":"RSA1_5","kid":"123"}, // Encryption key encrypted using 123's public key. "encrypted_key": "UGhIOguC7IuEvf_N...XMR4gp_A" }, { "header": {"alg":"A128KW","kid":"456"}, "encrypted_key": "6KB707dM9YTIgHt...2IlrT1oOQ" } ], "iv": "AxY8DCtDaGlsbGljb3RoZQ", // Encrypted message. "ciphertext": "KDlTtXchhZTGufMYmO...4HffxPSUrfmqCHXaI9wOGY", // AEAD authentication tag. "tag": "Mz-VPPyU4RlcuYv1IwIvzw" }
- Le chiffrement asymétrique a une taille limite de message (modulo – padding). C’est pour cette raison que l’on génère une clé symétrique à la volée qui est ensuite chiffrée avec la clé publique asymétrique.
11.10.4. JWT
11.10.4.1. Description et fonctionnement de JWT
- JWT définit la structure d’un token (chiffré, signé ou non sécurisé) permettant de véhiculer des “claims” standards, publics ou privés.
https://www.iana.org/assignments/jwt/jwt.xhtml
- Pour faciliter la transmission de tokens JWT, ce dernier est sérialisé dans un format compact (qui est également applicable à JWE et JWS).
- Chaque bloc (header / payload / signature etc…) est encodé en base64 URL-safe et séparé par un “.”.
- Exemple :
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOmZhbHNlfQ.FMy5mxG5mDjL4rW8defHN2fZ_U_ypDW6hUT-Oan2F6P36NzCEHq85IXWUChQc5vzCXa_SHWK9j1ZZG3vRwuEkEH-lA_FNPL2EAQjdqq_qxMhaS5SscW8RVb30rd7lw1-OvEESrKcAtqipDmkufpsv3R3YWBItF1Uev0wF1U9QGU
- Header
-
{"alg":"RS256","typ":"JWT"}
- Payload
{"sub":"1234567890","name":"John Doe","admin":false}
- Signature
14ccb99b11b99838cbe2b5bc75e7c73767d9532a435ba8544ce6a7d85e8fdfa3730841eaf392175940a141ce6fcc25da48758af63d59646def470b849041e500534f2f610042376aaaac4c85a4b94ac716f1155bdf4addee5c353af1044ab29c02daa2a439a4b9fa6cbf7477616048b45d547afd3017553d4065
11.10.4.2. Usages et avantages
- Les tokens JWT sont le plus souvent utilisés pour l’authentification et l’autorisation. (Ex. : SSO, session, web sockets, email links, etc…).
- Ils peuvent également être utilisés pour échanger ou stocker des données chiffrées ou signées.
- Contrairement à des formats binaires ou des formats XML tel que SAML, les tokens JWT sont :
- relativement légers.
- faciles à manipuler (librairies disponibles dans de nombreux environnements).
- faciles à manipuler et à archiver (Ex. : stockage dans une base MongoDB).
11.10.4.3. Utilisation de JWT pour l’authentification
- Vu que le token JWT contient toutes les informations nécessaires concernant l’identité du “Resource Owner” et qu’il est signé, il n’est pas nécessaire de vérifier le token auprès d’une base de données ou un service distant. Cela peut augmenter considérablement les performances et la “scalability” du service si aucun autre mécanisme (de cache par exemple) n’est mis en place.
- La plupart des tokens JWT contiennent des informations liées à au “Resource Owner” et sont le plus souvent stockées sur le device de l’utilisateur (Ex. : Local Storage).
- Il est donc recommandé de générer des tokens JWT signés puis chiffrés mais cela augmentera considérablement la taille du token qui sera envoyé à chaque requête authentifiée.
11.10.5. JWT, authentification, sessions et risques sécurité
- Avant d’aborder les aspects sécurité, les tokens JWT utilisés pour l’authentification ou la gestion de session sont accompagnés des problèmes suivants :
- Taille importante (particulièrement en activant le chiffrement).
- Immutabilité des “claims”. Il faut générer de nouveaux de tokens JWT pour transmettre les valeurs mises à jours des “claims”.
- No key policy : JWT ne définit aucune contrainte de sécurité concernant la gestion des clés (génération des clés symétriques, rotation des clés etc…).
- HMAC n’est pas un algorithme de signature : De nombreuses documentations et implémentations utilisent le HMAC pour authentifier les tokens JWT en parlant de signature.
- Pas d’invalidation : Bien que les tokens JWT peuvent contenir une date d’expiration, JWT ne peut définir aucun moyen pour révoquer ou invalider un token JWT.
- Euh… comment gérer le “logout” ?
- La seule solution possible est de stocker une information quelque part (liste des tokens invalidés, heure de logout, etc…)
- Cela nécessite alors de vérifier cette information à chaque présentation d’un token. On perd alors la plus grande partie de l’intérêt de l’utilisation des tokens JWT.
Politique de sécurité des clés privées TLS
- Analysons la politique de sécurité généralement associée aux clés privées TLS.
- Il est généralement recommandé (et de plus en plus pratiqué) d’utiliser une machine dédiée au chiffrement/déchiffrement des échanges TLS. Ainsi, si une application est compromise, la clé privée en mémoire n’est pas dévoilée.
- Les clés sont renouvelées régulièrement.
- Les clés sont protégées par des passphrases qui nécessitent parfois la présence de plusieurs personnes qui détiennent différentes parties de la passphrase.
- Que se passe-t-il en cas d’usurpation d’une clé privée TLS ?
- L’attaquant doit combiner cette attaque avec une attaque de type Man-In-The-Middle (ARP Poisoning, DNS Cache Poisoning…).
- L’attaquant ne pourra impacter généralement qu’une partie géographique donnée et uniquement les utilisateurs connectés pendant la durée de l’attaque.
- Dès détection, il est possible de révoquer rapidement le certificat associé et grâce à des protocoles tels que l’OCSP, les clients refuseront ce certificat.
https://tools.ietf.org/html/rfc6960
Risque d’usurpation des clés privées JWT
- Analysons maintenant une mise en place classique d’une authentification JWT.
- La clé privée du serveur d’authentification est malheureusement souvent stockée dans une variable d’environnement, une base de données ou encore un fichier (en espérant qu’il ne finisse pas sur le Version Control System).
https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/wiki/Key-generation
http://django-oidc-provider.readthedocs.io/en/v0.4.x/sections/serverkeys.html
- La clé privée peut être dévoilée de différentes façons :
- Accès au Version Control System.
- Injection SQL.
- Insecure remote file access.
- Dump des variables d’environnement en cas d’erreur.
- Si l’attaquant récupère la clé privée, il peut simplement forger des tokens JWT avec des “claims” arbitraires. Il peut alors récupérer les données de tous les utilisateurs dont l’authentification repose sur JWT.
“none” alg
- Malheureusement, un token JWT peut également utiliser un algorithme “none” qui n’est donc ni chiffré ni signé.
- L’attaquant peut donc forger des tokens JWT avec la valeur “none” pour la propriété “alg”.
- Si l’implémentation de vérification du token se base sur la propriété “alg”, elle est alors vulnérable et peut éventuellement accepter des tokens utilisant l'”alg” “none”.
- Certaines implémentations peuvent être vulnérables à une attaque qui consiste à utiliser la valeur “HS256” pour la propriété “alg”. L’implémentation utilise alors la clé publique RSA comme clé symétrique pour vérifier le HMAC.
11.10.6. JWT : Recommandations
- Il faut utiliser des clés RSA pour la signature.
- Il faut instaurer une rotation régulière et automatique des clés. Les clés publiques doivent être publiées automatiquement également.
- Etant donné que la rotation doit avoir une durée plus longue que la durée de vie des tokens, il faut réduire la durée de vie des tokens. (Ex. : l’implémentation OpenID Connect de Google semble appliquer une rotation de 3 à 4 jours mais je recommanderais une durée encore plus courte)
- Pour réduire les risques, utilisez de nombreuses clés.
- Idéalement, les clés secrètes ne devraient être manipulées que par des services dédiés (micro-services ?) hautement sécurisés avec des mécanismes de monitoring avancés.
- Les tokens JWT peuvent être utilisés comme mécanisme complémentaire d’un mécanisme de token classique. On peut wrapper des tokens dans un token JWT afin de vérifier rapidement leur validité et leur expiration avant de le vérifier auprès d’une base de données ou d’un tiers.
11.11. OpenID Connect
- OpenID Connect (OIDC) est un surcouche d’OAuth 2 permettant d’ajouter de nouvelles fonctionnalités concernant l’authentification et l’identification.
http://openid.net/connect/
- C’est un standard de la OpenID Foundation.
- OpenID Connect est donc compatible avec les implémentations OAuth2.
- On retrouve enfin de nombreux concepts intéressants similaires à ceux de “feu” Liberty Alliance Project : http://www.projectliberty.org/
11.11.1. Terminologie
- OpenID Provider : OAuth 2 Authorization Server capable d’authentifier l'”End-User” (Resource Owner) et transmettre des “claims” au “Relying Party” (Client).
- Relying Party : OAuth 2 Client capable de demander des “claims” à l'”OpenID Provider”.
- End-User : OAuth 2 Resource Owner.
11.11.2. Quoi de neuf ?
OpenID Connect fournit les fonctionnalités supplémentaires suivantes :
- Le “Relying Party” peut demander à l'”OpenID Provider” d’authentifier ou réauthentifier l'”End-User”.
- Il peut transmettre des informations supplémentaires (hint) comme l’identifiant du “End-User” pour faciliter la phase d’authentification.
- Utilisation de tokens JWT mais on peut éviter de les transmettre au “End-User”.
- “Claims” distribués et agrégés.
- Les données du “End-User” sont souvent distribuées entre plusieurs OpenID Providers.
- Avec OpenID Connect, un “OpenID Provider” peut agréger les “claims” ou fournir toutes les informations nécessaires pour les récupérer chez un d’autres OpenID Providers.
- Logout : Lorsqu’un “End-User” logout de l'”OpenID Provider”, ce dernier peut notifier les “Relying Parties” par différents mécanismes.
- “Dynamic Client Registration” : Certains “OpenID Providers” peuvent activer cette fonctionnalité permettant à des “Relying Parties” de s’inscrire dynamiquement.
- “Discovery” : L'”OpenID Provider” peut fournir publiquement des informations permettant aux autres entités (“Relying Party”, “OpenID Provider”, …) de découvrir dynamiquement les fonctionnalités et la configuration de l'”OpenID Provider”.
https://accounts.google.com/.well-known/openid-configuration
- “OpenID Connect” définit quelques “claims” supplémentaires.
- “OpenID Connect” définit quelques “scopes” qui englobent plusieurs “claims”.
11.11.3. OpenID Connect Flows
OpenID Connect définit 3 flows :
- Authorization Code Flow identique à celui d’OAuth 2 mais on y ajoute quelques paramètres supplémentaires et l'”OpenID Provider” retourne un “id_token” qui est un token JWT.
- Dans ce flow, le token JWT est échangé directement entre l'”OpenID Provider” et le “Relying Party” sans passer par le “User-Agent”. La signature du token est simplement une sécurité supplémentaire au dessus de la sécurité du canal TLS.
- Implicit Flow identique à celui d’OAuth 2 mais il fournit également un “id_token“.
- On retrouve les mêmes risques et inconvénients qu’avec OAuth 2 avec le risque supplémentaire lié au fait que l'”id_token” est un token JWT.
- Hybrid Flow consiste à fusionner les deux flows.
11.11.4. Que faire ?
- Malheureusement, OpenID Connect ne définit aucune règle concerne la signature des tokens JWT, le stockage et la rotation des clés.
- OpenID Connect est l’un des standards les plus avancés actuellement sur les aspects authentification, autorisation et gestion d’identité en général.
- Il faut idéalement éviter le flow “implicit”.
- Il faut utiliser des clés asymétriques.
- Il faut mettre en place une rotation régulière des clés.