Symfony live 2017 – Sécuriser nos API avec JWT

André Tapia nous a présenté ce standard qui commence à être bien connu et qui peut être couplé à l’oAuth. JWT est un standard qui repose sur une RFC qui fournit un moyen d’authentification (repose sur un token sécurisé) pour webservices, mais sans gestion des ACL (contrairement à l’oAuth).

Voici le pdf de la conférence.

[su_spacer]

Définition d’une API web

Pour démarrer, André nous a rappelé les principales caractéristiques d’une API web:

  • elle expose de l’information potentiellement critique
  • elle permet de manipuler cette information (ajout, édition, suppression)
  • elle permet de faire des appels en masse
  • elle doit être sécurisée dès la phase de développement. En cas d’information corrompue (si aucune sécurisation), elle devient inutilisable
  • elle doit être stateless: pas de session, chaque appel est isolé. Pas de redis ni de memcache (l’authentification est nécessaire à chaque appel)
  • elle doit être utilisée avec le protocole HTTPS afin de rendre plus difficile la capture des informations échangées et éviter des attaques de type man in the middle

[su_spacer]

Quelles solutions pour sécuriser une API web?

[su_spacer]

Une authentification basée sur la session

L’utilisateur va se connecter avec son login/mot de passe. Côté serveur, il va être identifié et un id de session est généré. Une réponse, qui contient cet id, est retournée à l’utilisateur. Il est alors stocké dans un cookie. Pour tous les appels suivants, grâce à cet id de session, l’utilisateur sera identifié sur le serveur.

L’inconvénient d’une telle pratique est triviale:

  • CORS (Cross-origin resource sharing): si les noms de domaine entre webservices sont différents, le problème de CORS va se poser
  • En terme d’évolutivité, la session va être stockée quelque part sur le serveur (ce qui viole le principe stateless d’une API web). Des problèmes de scalabilité vont rapidement se poser (il y aura stockage des sessions sur un lecteur partagé type nfs pour que toutes les instances des serveurs web puissent partager les mêmes informations)

[su_spacer]

Une authentification basée sur les clés d’API

Le principe est simple: on va générer une clef aléatoire associée à un compte utilisateur. L’utilisateur devra utiliser cette clef API à chaque appel pour s’authentifier.

Point positif: pas de session
Points négatifs:

  • Gestion des clefs en base de données
  • Pas de mécanisme d’expiration (le token sera valable indéfiniment). Il faudra trouver le moyen de régénérer une nouvelle clef à un moment donné, d’invalider la précédente, etc.
  • Token non exploitable, juste associé à un compte utilisateur

[su_spacer]

Solution idéale

  • Stateless (donc pas de cookies ni de session)
  • Gestion de l’expiration possible (un token devrait être généré à la demande et uniquement valable sur un laps de temps donné)
  • Auto porteuse et sécurisée (le vecteur d’authentification qui contient les données qui vont permettre au serveur d’authentifier l’utilisateur devra à la fois être accessible par le client et par le serveur)

[su_spacer]

Présentation du JWT (Json Web Token)

  • Le JWT est un standard industriel qui repose sur une RFC (7519)
  • Il permet de fournir une mécanisme d’authentification fiable pour l’ensemble des actions web basées sur des webservices
  • Il repose sur un token qui va contenir les données permettant d’authentifier l’utilisateur courant et qui va contenir d’autres données qui vont servir au client (comme son nom ou son rôle par exemple)
  • Le token généré est sécurisé. Deux possibilités pour ça: JWS (Json Web Signature) pour le signer, JWE pour le chiffrer (Json Web Encryption)
  • Il fournir un système d’expiration (durée de vie – time to live – déterminable)

[su_spacer]

A quoi cela ressemble-t-il?

Le JWT est une longue chaine de caractères découpée en trois parties séparées par un point:

[su_spacer]

1) Un header composé de deux champs : alg (algorithme de hashage utilisé, ici HS256) et typ (le type de jeton, ici « JWT »)

[su_spacer]

2) Un payload, qui contient les propriétés de notre jeton. On a des propriétés publiques, privées et réservées.

Voici la liste des propriétés réservées :

  • sub pour le subject
  • exp pour l’expiration time (si on définit un timestamp, au moment de la validation du token, on va avoir le moyen de savoir s’il est expiré ou non)
  • nbf pour not before (le token ne sera valable qu’à partir d’une date donnée)
  • iat pour issued at (générée au moment où l’on génère le token pour dater le token)
  • iss pout issuer (pour indiquer qui est l’émetteur du token)
  • aud pour audience (pour indiquer qui est le destinataire du token)
  • jti pour JWT ID (permet de définir un identifiant unique)

JWT n’impose rien. L’ensemble de ces sept propriétés est facultative. On est libre de les utiliser ou non. Ici, on a utilisé « sub » et « exp », ainsi que deux propriétés privées, « name » et « roles ».

[su_spacer]

3) La signature

On va alors utiliser l’algorithme de hashage défini dans le header (HS256 pour HMAC + SHA256).

On prend deux paramètres:

  • base64UrlEncode du header
  • base64UrlEncode du payload + une clef secrète.

Signer le token permet de vérifier que les données n’ont pas été altérées.

Il y a trois algorithmes de hashage possibles :

  • HMAC + SHA
  • RSA + SHA
  • ECDSA+ SHA

JWT laisse le développeur choisir l’algorithme de hashage.

[su_spacer]

Exemple d’une application mobile utilisant une API

[su_spacer]

Etape 1:

  • L’utilisateur va s’authentifier sur l’API (sur api/login, par exemple) avec son login et son mot de passe habituel
  • Le serveur va vérifier que le couple est correct
  • En cas d’authentification réussie, le serveur génère et renvoie un token JWT à l’application avec le code 200

[su_spacer]

Etape 2:

Pour les appels suivants, l’application transmet le token JWT pour chaque transaction suivante en header des requêtes (header Authorization : Bearer valeurTokenJWT) => le serveur authentifie l’utilisateur, vérifie avant que les données du token n’ont pas été altérées

Cependant, comme JWT n’impose rien, ni aucune mesure n’est prise, un token n’a pas de date d’expiration et est donc toujours valable. Si un utilisateur se logue une première fois, obtient un token puis se logue une seconde fois, il aura un nouveau token mais le précédent n’aura pas été invalidé. Si une activité suspecte est détectée côté serveur, peu importe si on modifie les rôles ou autres éléments du compte utilisateur, le token généré avant l’étape de vérification sera toujours valide, ce qui constitue une grosse faille de sécurité.

[su_spacer]

Gestion de l’expiration

Concernant l’expiration, pas de durée type. En moyenne, on la définit entre 5 min et une heure. Une fois le délai expiré, une réponse http avec le code d’erreur 401 est retournée pour permettre au client de savoir que le délai est expiré et qu’il va devoir se réauthentifier.

Cependant, comprenez bien que l’on ne peut demander à l’utilisateur de se réauthentifier toutes les heures. La solution: le refresh_token!

Lorsque l’utilisateur se logue, un refresh_token lui est renvoyé en plus du jeton. Côté application, on stocke tout ceci en local.

[su_spacer]

A un moment donné, le token est rejeté pour cause d’expiration.

[su_spacer]

A ce moment là, une autre url sera appelée (api/token/refresh, par exemple) et utilisera le refresh_token envoyé au départ (il y aura bien sûr vérification prélable qu’il est toujours valable). Un nouveau token est généré et stocké en base pour les futurs appels (le refresh_token, tant qu’il est valide, n’est pas changé et est renvoyé dans la réponse avec le nouveau token généré)

Un refresh_token a une durée définie (1 mois, par exemple). A son échéance, il faudra forcément se réauthentifier, ce qui aura pour effet de regénérer un token et un nouveau refresh_token.

[su_spacer]

Intégration dans un projet Symfony

Il existe depuis Symfony2.8 une classe abstraite: AbstractGuardAuthenticator (qui va permettre de générer le token d’identification) qui implémente l’interface GuardAuthenticatorInterface qui étend elle-même une autre interface AuthenticationEntryPointInterface.

L’interface GuardAuthenticatorInterface comporte 7 méthodes qui seront toutes appelées:

  • getCredentials: permet d’extraire les informations de la requête pour en récupérer les login/mot de passe
  • getUser: permet de récupérer l’utilisateur en fonction des identifiants utilisés
  • checkCredentials: permet de vérifier les identifiants
  • createAuthenticatedToken: permet de générer le token d’authentification
  • onAuthenticationSuccess: renvoie un objet réponse si l’authentification est réussie
  • onAuthenticationFailure: renvoie un objet réponse si l’authentification a échouée
  • supportsRememberMe: permet de vérifier si notre système d’authentification accepte les cookies

L’interface AuthenticationEntryPointInterface ne comporte qu’une méthode (start) qui sera appelée lorsqu’un utilisateur dit anonymous essayera d’accéder à une ressource nécessitant une authentification.   Le but de cette méthode est de renvoyer une réponse qui «aide» l’utilisateur à commencer dans le processus d’authentification.

Exemples:

  • Pour une connexion au formulaire, on peut rediriger vers la page de connexion et retourner une RedirectResponse (‘/ login’);
  • Pour un système d’authentification par jeton API, on peut renvoyer
    new Response('Auth header required', 401)

Nous allons voir comment appeler ses méthodes en suivant trois étapes:

  1. Création d’un utilisateur
  2. Authentification
  3. Retour des news en interrogeant /api/news

[su_spacer]

Création d’un utilisateur et configuration des accès

On commence par définir un encoder et un utilisateur avec ses login/mot de passe.

[su_spacer]

Puis nous mettons en place le firewall avec les handlers de succès et d’échec (les noms correspondent aux services définis ci-dessous dans services.yml).
Enfin, nous indiquons par l’access_control que l’url api/login peut être accédée sans être authentifié.

[su_spacer]

Authentification

En cas de mauvaise authentification, on appelle la méthode onAuthenticationFailure de la classe AuthenticationFailureHandler (qui implémente l’interface AuthenticationFailureHandlerInterface qui oblige à redéfinir la méthode onAuthenticationSuccess) et on retourne une 403

[su_spacer]

Si l’authentification est correcte, on appelle la méthode onAuthenticationSuccess de la classe AuthenticationSuccessHandler (qui implémente l’interface AuthenticationSuccessHandlerInterface qui oblige à redéfinir la méthode onAuthenticationSuccess).
On instancie simplement simpleJMWS en lui passant en argument l’algorithme de hashage.
On définit également le payload avec une date d’expiration d’une heure.
Puis on signe le jeton et on retourne une JsonResponse avec le token.

 

[su_spacer]

Liste des news

[su_spacer]

Il est alors temps de créer notre authenticator en implémentant les différentes méthodes vues au début de l’article.

[su_spacer]

On implémente la méthode getCredentials().

[su_spacer]

S’il n’y a pas d’entête Authorization, on appelle la méthode start() qui renvoie une 401, puisque la ressource /api/news en GET est protégée et requiert une authentification.

[su_spacer]
En cas de succès de la méthode getCredentials(), la méthode getUser() sera appelée.

[su_spacer]
En cas de succès de getUser(), la méthode checkCredentials() est appelée pour voir si les données du token n’ont pas été altérées.

[su_spacer]
En cas d’échec de getUser(), on appelle la méthode start() qui renvoie une 401.
En cas de succès de checkCredentials(), la méthode onAuthenticationSuccess() est appelée.
En cas d’échec de checkCredentials(), la méthode onAuthenticationFailure() est appelée.

[su_spacer]
On implémente la méthode supportsRememberMe() qui retourne false comme nous sommes dans le cas d’une API stateless.

[su_spacer]

N’oublions pas de l’enregistrer dans security.yml:

        # app/config/security.yml
        security:
            # ...
            firewalls:
                # ...
                login:
                    # ...
                    guard:
                        authenticators:
                            - app.token_authenticator

Enfin, lors de l’appel de /api/news, on passe dans l’entête Authorization « Bearer monTokenJson » pour pouvoir accéder à la ressource.

[su_spacer]

Une autre solution

Il est possible de s’épargner une partie du développement en couplant deux bundles: LexikJWTAuthenticationBundle avec JWTRefreshTokenBundle.

[su_spacer]

Conclusion

JWT n’est pas exempt de défauts, mais si l’on s’appuie sur une bonne librairie et un bon système de signature, si on a une date d’expiration sur notre token, on a réuni tous les ingrédients pour avoir une solution robuste et fiable.

JWT ne sert pas qu’à l’authentification mais aussi pour vérifier que les données n’ont pas été altérées (comme le nom/prénom, la date, etc.)  grâce à la signature.

Plus d’exemples ici: https://knpuniversity.com/screencast/guard

Rédigé par

2 comments

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.