Identité et autorisation
Normalement, dans un API fermé, nous allons empêcher l'accès aux utilisateurs qui n'ont pas le droit d'y accéder.
Comme pour la sécurisation d'une installation Linux, il y a plusieurs facettes à la sécurisation d'un API :
identité
ressource
droits
Identité
Avant de se lancer dans la sécurisation d'un API il faut identifier d'abord QUI aurait accès à votre API.
Surtout, comment notre API va être capable d'identifier un utilisateur précis, et être 100% certain que c'est bien elle qui fait des requêtes.
Aujourd'hui, il existe plusieurs façons (flux ou identity flows) pour établir et vérifier l'identité d'un utilisateur.
Le principe distillé est le suivant :
à la première connexion, on demande à l'utilisateur de s'identifier par une valeur unique. Normalement, c'est une adresse e-mail ou nom d'utilisateur qu'il aurait choisi (ou qui lui a été attribué) à la création de son compte
Ensuite, l'utilisateur est demandé de prouver son identité via une valeur ou secret seulement à la disposition de cet utilisateur
L'API crée un jeton unique valable uniquement pendant la séance, et on passe ce jeton à l'utilisateur (via un cookie, par exemple)
Le client de l'utilisateur doit désormais fournir ce jeton avec chaque requête
À la réception d'une requête, l'API doit valider le jeton, et rejeter la requête si le jeton n'est pas présent ou n'est pas valable, ou bien si l'utilisateur n'a pas le droit d'accéder à la ressource demandée
Prouver son identité
Comment l'utilisateur peut prouver son identité ?
Un mot de passe connu uniquement par l'utilisateur
Une valeur biométrique physique de l'utilisateur (emprunt digital, iris, identification visage, etc.)
La solution d'un mot de passe est compliquée pour plusieurs raisons :
Un mot de passe trop simple est facile à deviner grâce aux algorithmes simples (attaque en force brute)
Un utilisateur a tendance à oublier son mot de passe complexe. Il réutilise donc le même mot de passe partout
Nous sommes obligés de stocker le mot de passe dans notre base de données.
Si on est hacké, on divulgue le mot de passe de nos utilisateurs au hackeur.
À ce moment-là, le hackeur aurait potentiellement accès à tous les comptes utilisant le mot de passe partagé
La solution des biométriques est compliquée aussi :
On a besoin d'un périphérique spécial (lecteur d'emprunt digital)
Sinon, certaines méthodes ne sont pas totalement fiables (détection faciale) et pourraient engendrer des faux positifs ou des faux négatifs
Identité par délégation
Une autre stratégie pour prouver l'identité est de dépendre sur un tiers.
Par exemple, si on utilise l'adresse e-mail de la personne, on peut supposer que seulement cette personne aura accès à sa boîte mail.
Pour prouver son identité, on pourrait juste forcer cet utilisateur à prouver qu'il a accès aussi à sa boîte e-mail - en l'envoyant un message à cette adresse.
Normalement, on envoie un émail avec un code ou lien unique. Si l'utilisateur peut nous répéter ce code unique, il prouve qu'il a pu consulter son mail.
Pour nous, cela veut dire qu'on ne stocke plus son mot de passe dans notre base de données ! On dépend du mot de passe utilisé pour protéger son compte email :
C'est 1 mot de passe en moins à mémoriser pour l'utilisateur
En revanche, si le mot de passe de sa boîte email est volée, le voleur aura accès à notre service aussi
Ce flux pourrait être adapté autrement : par envoi d'un SMS par exemple.
Un autre type d'identité par délégation existe aujourd'hui grâce à la norme OAuth2. L'idée de base est qu'un service centralisé s'occupe de l'identification d'un utilisateur, et crée des jetons d'accès utilisables par notre API. On en a tous utilisé :
connexion avec votre identifiant Google
connexion avec votre identifiant Facebook
connexion avec votre identifiant GitHub
En revanche, cela force nos utilisateurs d'avoir un compte chez un tiers avant d'utiliser notre service (parfois pas idéal). Et, aussi, s'il y a une fuite chez un de ces services, notre API sera à risque aussi.
La double authentification
Pour encore sécuriser nos services aujourd'hui, la tendance est d'aller vers une combinaison des différentes approches :
Je fournis mon adresse e-mail et mot de passe
Si le mot de passe est validé, l'API envoie un message avec un code unique à l'adresse email
l'utilisateur doit répéter le code unique trouvé dans la boîte mail
le jeton est créé
... etc
Même si le premier mot de passe est divulgué, on a une autre couche de protection via le mot de passe de la boîte mail.
Autorisation des Ressources
Une fois l'identité connue, il faut maintenant préciser ce qu'on va protéger dans notre API. Certains endpoints qui divulgue des informations sensibles doivent être protégés.
Avec Express, ce processus est simple, il suffit d'ajouter un middleware devant une route, et devant une branche dans notre arborescence. Ce middleware va décoder le jeton d'accès pour vérifier l'identité de la personne et les droits qui lui sont accordés.
Un exemple middleware qui cherche le jeton d'accès sur l'en-tête authorisation
:
Nous utilisons ce middleware en le plaçant à la tête des branches à protéger dans notre API :
Droits
Dans beaucoup d'APIs, il n'est pas suffisant de savoir que la personne est bien identifiée. Cet utilisateur aura peut-être des droits différents selon son rôle. Par exemple, un utilisateur normal vs un administrateur.
Une stratégie serait d'encoder dans le jeton le rôle de l'utilisateur, ou bien un indice des ressources à sa disposition.
On parle souvent de son scope, c'est l'ensemble de ressources à la disposition de l'utilisateur, qu'on aura extrait de la base de données lors de la création de son jeton d'accès. Un scope pourrait être simplement un string :
admin
: un scope global qui donne accès à toutuser
: l'utilisateur aura accès à son profil utilisateurbidule
: à nous de définir ce que cela veut dire
On peut adapter notre middleware afin de prendre en compte des scopes :
Ensuite, pour chaque branche de notre API on peut facilement préciser les scope nécessaires pour accéder à la branche :
Le jeton d'identité
Le jeton d'identité correspond à quoi exactement ?
Un numéro unique (un UUID par exemple)
On crée et stocke un identifiant lié à la session de l'utilisateur. À chaque requête, l'API est obligé de chercher dans sa base locale l'identité de l'utilisateur et les droits associés. Ceci n'est pas très compatible avec une API totalement stateless, parce qu'on est obligé de stocker le UUID quelque part entre les requêtes
Un jeton qui stocke toutes les informations d'identité et d'accès : le JWT
Un JWT (JSON Web Token), et un format qui permet de transmettre non-seulement des informations d'identité, mais aussi les scopes en un seul paquet. En plus, le JWT est signée, c'est-à-dire, on a le moyen de valider l'identité de la personne qui la crée initialement (nous-mêmes !).
Le JWT contient aussi une expiration — elle s'expire toute seule au-delà d'une certain période.
Le flux d'un JWT est le suivant :
L'identité est validée
On crée une structure (le payload) contenant le ID et les droits de l'utilisateur
On crée le JWT contenant notre structure d'information, et d'autres informations obligatoires pour un JWT (expiration, issuer, audience, etc)
On signe la JWT avec notre clé privée : on attache au JWT une partie contenant cette valeur chiffrée
On envoie la JWT à l'utilisateur (le JWT contient la partie en texte clair et la partie chiffrée)
Le client de l'utilisateur attache le JWT à l'en-tête d'une requête
L'API récupère le JWT de l'entête, et essaye de le décrypter en utilisant la clé publique correspondant à la clé privée. Si la valeur décryptée corresponde parfaitement à la partie en texte claire, on est sûr que c'est nous qui l'aurons créé
On valide que le JWT n'est pas encore expiré
On récupère les scopes et l'ID de l'utilisateur et on valide l'accès à la ressource
Pour un JWT sécurisé, il faut donc une paire de clés qui permet de chiffrer et déchiffrer le JWT. Il y a une librairie NodeJS qui nous aide à gérer les JWT qui s'appelle jsonwebtoken
.
Le code suivant est un exemple d'une classe utilitaire qui permet de créer et décoder un JWT :
Exercice : Autorisation "magique"
Dans vos challenges, vous avez pu vous connecter juste en renseignant votre adresse email (sans mot de passe). Un mail avec un code est envoyé à votre boîte mail. Quand on clique dessus, on est considéré autorisé.
Typiquement le mail contient un URL cliquable qui contient le JWT codé en hexadecimal comme paramètre query. En cliquant sur ce lien, un navigateur s'ouvre sur un endpoint de notre API. L'api récupère le paramètre query (le JWT), le décode, et si toutes les informations sont correctes, on peut supposer que l'identité de l'utilisateur est prouvée.
On peut ensuite générer un autre jeton type access token
qui :
contient le ID de l'utilisateur
contient les scopes de l'utilisateur
Mettez en place ce flux avec les contraintes suivantes :
Utilisez Mailjet pour l'envoi de votre mail. Mailjet dispose d'un palier gratuit, et il y a même une librairie nodejs qui facilite l'interaction avec leurs APIs
Le lien dans le mail doit avoir un timeout de 5 minutes, au-delà de cet intervalle, il faut retourner une erreur comme quoi, il faut redemander un nouveau mail.
On ne doit pas pouvoir accéder aux autres endpoints dans notre API avec le jeton dans le mail. En effet, il faut que le jeton de connexion magique soit uniquement pour valider l'identité de l'utilisateur. Ensuite, un autre jeton est créé pour l'accès aux endpoints de l'API. Ce dernier pourrait contenir d'autres infos dans son payload, ou bien être chiffrée en utilisant une paire de clés différente.
Dernière mise à jour