Le TOKEN API: Simple, JWT et macaron

TL;DR Le macaron, c’est un JWT sous stéroïde…

Si vous créez une application, vous aurez probablement (en tout cas j’espère pour vous) des clients qui vont la consommer.

Si c’est une API, vous allez devoir fournir à vos utilisateurs un token qu’ils devront rajouter dans le header (Authorization: bearer MON_TOKEN). Grâce à cela vous allez pouvoir les identifier mais surtout savoir ce qu’ils ont le droit faire (ou pas).

Mais qu’est-ce qu’on met dans ces tokens? Comment les génère-t-on? Quid de la vérification des droits? Que fais-je? Pourquoi-je?

1. La préhistoire

Dans un cas basique, un token est une bête chaine de caractère plus ou moins longue, liée à un « user id » dans une base de données. Que l’on soit en mode « monolithe » ou « micro service », nous devrons probablement appeler un service d’authentification (user gateway) centralisé pour vérifier la validité du token.

Une fois l’utilisateur authentifié, le backend prend le relais et peut, le cas échéant, refaire des requêtes au « user gateway » pour savoir si l’utilisateur a les droits pour effectuer telle ou telle action. Pour  limiter l’accès au « user gateway », nous allons probablement l’optimiser pour qu’il nous renvoi l’identité de la personne et toute une série de flags correspondant à tous ses droits.

Autant vous le dire tout de suite, notre « user gateway » est un SPOF (single point of failure): S’il crash, c’est terminé! Aucun de nos micro-services ne répondra ou  notre monolithe sera down.

Ça fait également beaucoup des requêtes sur un réseau et qui dit réseau dit: Lent et non-fiable. En mettant du cache, on s’expose à d’autres problème car c’est bien connu, le cache est la chose la plus complexe en informatique…

2. Le JWT? C’est « sooo 2013 »

Ensuite, il y a le Json Web Token (JWT, pour les intimes). Avec lui, ce qui est cool c’est qu’il permet d’embarquer un JSON (appelé payload) intégré dans le token. Ce n’est donc plus une chaine de caractères aléatoires.

Mais pourquoi aurait-on besoin d’un json dans un token?
Je vous donne un exemple: Nous n’aurons plus besoin d’une table de correspondance token<->userId car le userId sera livré dans le payload.

Mais si le userId est dans le JSON, on peut mettre ?
Oui

Mais c’est pas du tout « secure » ton brol?
Mais siii, allez, il y a quand même une sécurité: Tout le monde ne peut pas créer un token!

Comment crée-t-on un token JWT?

La recette est simple! Pour faire un token, nous avons besoin de:

  • Un payload: le JSON.
  • Une clé secrète. Elle peut être symétrique (un secret qui peut être partagé) ou asymétrique (clé publique/privée).
  • Un algorithme de signature. Au choix: HS256, HS512, RS256, RS512, ES256, ES512, etc…

Comme nous pouvons le voir, le format d’un JWT est formée de 3 parties, séparées par un point:

  • La première partie « header », est le « base 64 » d’un json qui indique l’algorithme utilisé
  • La deuxième partie « payload », est le « base 64 » du json que nous voulons envoyer (le userId, etc…)
  • La troisième partie « verify signature », est le « base 64 » de la signature de l’algorithme utilisé.

Comment vérifie-t-on un token JWT ?

Une fois le token envoyé au backend, il faut bien sûr pouvoir le vérifier. Pour ce faire, rien de plus simple, les librairies fournisse la méthode « verify » qui nécessite 3 arguments:

  • Le token
  • Le secret avec lequel le token a été signé
  • L’algorithme qui a permis de signé le token

Grâce à cette méthode, nous pouvons être sûr que le payload est valide car personne ne peut produire un JWT valide à moins de connaître la clé secrète.

Pour récupérer le payload sous format json rien de plus simple:

Le payload n’étant pas crypté, on peut le récupérer avant même d’avoir vérifié la signature du token. Cela peut être pratique dans certains cas. N’y mettez donc jamais de données sensibles (mot de passe, api key, etc…).

Résumé et pistes de réflexion à propos du JWT…

  • Grâce à JWT, on a l’occasion de passer plus d’informations valides grâce au payload.
  • Le payload peut être lu sans connaitre le secret
  • Il faut connaitre un secret pour signer ou vérifier un token… Ce qui implique parfois un secret partagé. A ne pas oublier toutefois: Au moins le secret est partagé, au mieux on se porte.
  • On ne peut pas modifier un token. Pour ce faire, il faut en créer un nouveau sur base d’un autre:
    • Extraire le payload
    • Rajouter/modifier/supprimer des données
    • Créer un autre token avec le même secret et le même algorithme… Il faut donc connaitre le secret.
  • Si le secret est compromis, il faudra en re-générer un autre, ce qui invalidera tous les tokens.
  • Idée: Pourquoi ne pas mettre la « version » du secret pour permettre la migration d’un secret à un autre? Une sorte de dépréciation.
  • Idée: Vu que le secret peut être asymétrique, Il est possible de créer une clé privée/publique par utilisateur. On génère un token avec la clé privée, et on enregistre la clé publique dans la base de données. Cette clé n’est pas critique et peut être volée sans rien compromettre. Ce qui donnerait ceci:
    • Récupération du token dans le header (ou un cookie, une session, etc…)
    • Extraction du champs userId dans le payload
    • Récupération de la clé publique liée à cet utilisateur
    • Vérification du token avec la clé publique
    • Si c’est bon, le token est confirmé et on peut faire confiance aux infos du payload

3. Le macaron, le serial(ized) killer ?

Le macaron est un type de token inventé par des mecs de Google en  2014. Si voulez le papier complet, vous pouvez le télécharger ici: https://ai.google/research/pubs/pub41892.

L’idée assez différente: On va créer un token (toujours signé avec une clé secrète), qui va contenir une liste de conditions (appelés caveats) qui vont permettre de savoir si celui-ci est valide. Un peu comme une liste de « if » super stricte: Le premier qui répond false rend le token invalide. Un caveat est toujours une chaine de caractères, du genre: »time < 2019-01-01″. Dans ce cas-ci, le token est valide jusqu’au 31 décembre.

Comment crée-t-on un macaron?

La recette est simple. Pour faire un macaron, nous avons besoin de:

  • Une location: L’adresse « http » de la ressource que nous voulons accéder. Cette adresse est purement indicative et n’impacte pas la validité du macaron. Il n’y a pas de format spécifique, on peut donc vraiment y mettre ce qu’on veut.
  • Une liste de caveats: conditions sous forme de chaine de caractères. Il n’y a pas de format spécifique.
  • Une clé secrète pour signé le tout
  • Un identifier (ou identifier key): C’est un « indice » sur la clé secrète utilisée pour signer le macaron. Par exemple, vous pouvez mettre « clé numéro 2 » et le backend saura qu’il faudra vérifier le token avec la clé secrète numéro 2. Comme pour la location, cette valeur est purement indicative et n’impacte pas la validité du token. Il n’y pas de format spécifique.

Qu’est ce qui se cache derrière ce gros pavé de base64:

Si on analyse ligne par ligne:

  • location: La location utilisée pour instancier le macaron.
  • identifier: Le fameux « indice » qui indique quel secret on utilise
  • « cid ip = 127.0.0.1 »:  « cid » veut dire « caveat identifier ». C’est une condition qui doit être correcte pour que le macaron soit valide. Si nous avons 2 caveats, il y aura deux lignes commençant par « cid ».
  • « signature »: La signature de l’encryption en base 64.

Comment vérifie-t-on un macaron ?

C’est bien joli tout ça, on a un ou plusieurs caveats qui fonctionnent comme des conditions, mais comment vérifier les conditions? Voilà le code qui vérifie notre macaron précédent:

En fait, ce n’est pas sorcier, la méthode satisfyExact doit recevoir la même chaine de caractères que la méthode addCaveat utilisée pour créer le macaron. Simple, basique! Par défaut, les librairies donne deux méthodes pour vérifier une condition:

  • satisfyExact: Elle ne fonctionne que pour la condition « machin est strictement égal à cette valeur ». Ex: ip = 127.0.0.1.
  • satisfyGeneral: Cette méthode, bien plus puissante, reçoit une fonction/callback en paramètre. Celle-ci sera appelée pour vérifier chaque caveat. Le caveat est d’ailleurs son seul argument. La fonction doit renvoyer « true » si le caveat est correct et « false » dans tous les autres cas.

Pour illustrer l’utilisation de la méthode satisfyGeneral, imaginons ce petit caveat tout simple:

Comme on peut le voir, ce caveat est un peu plus complexe à cause du « < » qui n’est pas implémenté par les librairies. On aurait pu l’écrire autrement (« time is lower than 2019-01-01 » par exemple), mais peu importe, car de toute façon, c’est nous qui allons devoir faire une fonction qui « parse » et vérifie ce caveat. Voici le code de notre « vérifieur »:

Maintenant que notre nouveau « vérifieur de caveat » est prêt, il suffit de  l’ajouter à la liste des « vérifieurs connus » via la méthode satisfyGeneral :

À présent, nous sommes prêts à recevoir un caveat de type « time < … ».

Passe, passe l’macaron!

La deuxième idée du macaron est assez cool: Une fois le macaron en notre possession, il est possible, sans connaitre le secret, de rajouter d’autres caveat. On va donc rendre ce macaron de plus en plus strict car de nouvelles conditions devront être remplies. Il n’est pas possible de modifier ou supprimer un caveat.

Imaginez un peu:

  • Un proxy peur rajouter des caveats sans connaitre le secret partagé.
  • Un utilisateur peut donner son macaron a quelqu’un d’autre en rajoutant un caveat du style « readonly ».
  • Un tier peut rajouter une notion d’expiration sur une ressource.
  • etc…

Moi j’aimerais bien donner des données aussi…

Jusqu’ici, on a vu qu’un macaron, ce n’est qu’une succession de conditions/caveats mais parfois, on aimerait bien pouvoir donner des données comme un JWT. Il y a deux possibilités:

  • Soit on utilise l’ identifier comme gros payload en format json. Vu qu’il n’y a pas de format, ce n’est pas contre indiqué.
  • Après une petite discussion avec Quentin Adam de Clever Cloud, il m’a gentillement montré un bout de code qui permet de faire passer des données via les caveats. L’idée est pas mal du tout: Vu que le format d’un caveat (et son « vérifieur ») peut être custom, nous allons créer un caveat qui dit « enregistre cette valeur dans cette variable lorsque tu vas me vérifier ». Le « vérifieur » va extraire la donnée et renvoyer true.

Voici un exemple:

Et le « vérifieur »:

Et voilà comment on peut s’en sortir pour extraire des données d’un macaron.

Résumé et pistes de réflexion à propos du macaron…

  • Le macaron, comme pour JWT, permet de s’assurer de l’authenticité du token
  • On peut rajouter des caveats sans même connaitre le secret: pratique pour les proxy ou pour partager un macaron avec moins de droits. Cela fait de lui un sérieux outsider car les secrets sont moins éparpillés.
  • Le macaron, comme pour JWT, permet aussi d’extraire des données. Vu que la récupération est custom: The sky is the limit! Peut-être une source d’inspiration?
  • Idée: Pour l’identifier (à la création du macaron), j’ai déjà vu des gens utiliser ce genre du format: « userId:secretType:nonce ». Ce qui permet de:
    • Connaitre facilement le userId.
    • Savoir quel type de secret on a utilisé: une clé publique? un secret partagé? Cela peut aussi changer en fonction des droits de l’utilisateur.
    • Le « nonce » est une valeur incrémentée à chaque requête. Cela permet de vérifier qu’on ne réutilise pas deux fois le même macaron… Pourquoi pas ?

 

Nous n’en avons pas fini avec les macarons. On reviendra, dans un autre article, sur une autre fonctionnalité plus obscure, mais super puissante: les « third party caveat ». SPOILER: En gros, un autre système vous donne un macaron que vous mixez avec le votre et paf, ça fait (des chocapics) un mix des deux. Vous vous retrouver, dès lors, avec deux autorisations en une.