Aller au contenu principal

Mettre en oeuvre JWT

Support de JWT pour authentifier les consommations d'API.

Avec cette fonctionnalité, les fournisseurs d'API peuvent permettre à leurs consommateurs d'utiliser un token JWT sous la forme d'une autorisation ou d'un cookie, afin d'améliorer les conditions sécuritaires de consommation de l'API, notamment dans un contexte d'utilisation de navigateur web.

Cas d'usages

  • simple : le consommateur demande la génération à l'API raccordée dans Okapi, et consomme l'API via le JWT obtenu en utilisant une autorisation ou bien le cookie sécurisé : nécessité d'ajouter une route (par exemple /jwt) de ressource utilisant le plugin generate-jwt
  • complexe : le consommateur demande à un serveur d'autorisation externe la génération d'un JWT, et l'utilise ensuite pour consommer l'API via Okapi : nécessité de déclarer la clé publique utilisée pour générer le JWT dans le raccordement de l'API.

Principe de fonctionnement

  • L'API exposée propose un endpoint permettant une génération du JWT, sous la forme d'une ressource virtuelle utilisant un plugin
  • L'API peut être consommée avec un JWT généré par un système tiers : dans ce cas le JWT devra avoir été généré avec un algorithme asymétrique et la clé publique devra être déclarée dans le raccordement de l'API
  • Le JWT généré, par Okapi ou par un système tiers, contient la clé d'app du développeur pour permettre à Okapi d'authentifier l'application comme elle le fait déjà
  • Le JWT lorsque généré par Okapi permet de définir des attributs du payload dans le raccordement
  • Le JWT supporte les claims standards

Tests d'acceptance

  • Given je dispose d'une clé d'app, when je consomme la ressource POST /jwt d'une API raccordée avec la feature JWT, then j'obtiens un JWT, un token Xsrf et un cookie

  • Given je dispose d'une autorisation ou d'un cookie JWT, when je consomme l'API avec l'autorisation ou le cookie JWT, then j'obtiens une réponse JSON sans erreur d'autorisation ou d'accès

  • Given je dispose d'un JWT expiré, when je consomme l'API avec l'autorisation ou le cookie JWT, then j'obtiens une erreur m'interdisant l'accès à la ressource

Spécifications {#spécifications}

  1. Dans le raccordement d'une API, support des attributs extra.jwt.[secret,expiresIn,payload,publicKey,privateKey,cookie] :
    • secret du jwt ou clé publique et privée
    • durée d'expiration au format https://github.com/zeit/ms (optionnel)
    • options jwt / claims (optionnels mais qui influencent la vérification) : algorithm, issuer, subject, audience
    • objet JSON à ajouter dans le payload (optionnel) pouvant contenir des données qui seront utiles à ceux qui consommeront le JWT (l'api, ou autre composant intermédiaire)
  2. Plugin de ressource "generate-jwt" ayant pour rôle de :
    • générer un JWT contenant l'appKey courante et les données du body de la requête
    • générer un token XSRF
    • l'ajouter dans un entête de réponse "X-Xsrf-Token"
    • ajouter à la réponse un cookie securisé et "http only" ayant pour nom "access_token", pour valeur le JWT et la même expiration que le JWT
    • répondre un JSON avec le token xsrf
  3. Dans le raccordement d'une API, possibilité d'ajouter une ressource dédiée à la génération du JWT, s'appuyant sur le plugin "generate-jwt"
  4. Dans l'api gateway :
    • si présent le cookie JWT est parsé et vérifié
    • vérification de l'égalité de l'entête "X-Xsrf-Token" avec celui extrait du JWT
    • extraction la clé d'app
    • chargement de l'app correspondante avant la consommation du middleware "load-app" pour continuer normalement le reste du process d'authentification

Principe de mise en oeuvre

  • L'API est raccordée avec des données qui caractérisent la façon de générer ou supporter JWT.
  • Dans le cas de l'utilisation d'un "secret", la génération et la consommation du JWT doit se faire par l'API.
  • Dans le cas de l'utilisation d'un couple clé privée / publique, il est possible de consommer l'API avec un JWT qui aurait été généré ailleurs avec une clé privée, dans ce cas il faut déclarer la clé publique (pas la clé privée) dans le raccordement mais il est inutile de déclarer un plugin de génération jwt car ce n'est plus de la responsabilité de l'API.
  • Dans tous les cas, le payload JWT doit contenir les données nécessaires à Okapi (appId + sandbox), dans le namespace "okapi", pour permettre d'identifier l'application consommatrice.
  • Le consommateur peut choisir d'utiliser le JWT via un entête "Authorization Bearer" ou bien via le cookie sécurisé retourné au moment de la génération par Okapi (dans ce cas la génération peut difficilement être tierce).

Bénéfices {#bénéfices}

  • Pas de ré-emballage de JWT qui nuirait aux performances et à la scalabilité
  • Possibilité d'utiliser le JWT pour authentifier l'app (besoin d'okapi), ainsi que le end user (besoin de l'API) avec le même dispositif
  • Ouverture maximale pour permettre une génération par Okapi, ou par un autre système (CCU, etc), tout en conservant un principe unifié d'authentification.
  • Possibilité de mieux sécuriser le contexte "web" du consommateur grâce à l'utilisation du cookie

Mode opératoire d'utilisation {#mode-opératoire-dutilisation}

# Prequisite : curl, jq

# Dans un 1er terminal, lancer l'API raccordée à "local demo"
$ http-request-spy

# Dans un 2ème terminal, lancer le serveur API gateway :
$ cd -P ~/OpenAPI/okapi; npm run start:api

# Dans un 3ème terminal, tester l'authentification et la consommation de l'API :
$ OKAPI_APP_KEY="maclédapp"
$ JWT_RES_BODY=$(curl -X POST -H "X-Okapi-Key: ${OKAPI_APP_KEY}" "http://localhost:3101/local/v1/jwt")
$ XSRF_TOKEN=$(echo ${JWT_RES_BODY} | jq -r ".xsrfToken")
$ ACCESS_TOKEN=$(echo ${JWT_RES_BODY} | jq -r ".accessToken")
$ curl -i -H "X-Xsrf-Token: ${XSRF_TOKEN}" -H "Cookie: access_token=${ACCESS_TOKEN}" "http://localhost:3101/local/v1/hello"
# # Observation : ici on ne passe plus la clé Okapi, mais à la place : le cookie jwt + un entête xsrf (dans un browser, il n'y a plus de secret visible)
$ curl -i -H "X-Xsrf-Token: ${XSRF_TOKEN}" -H "Authorization: Bearer ${ACCESS_TOKEN}" "http://localhost:3101/local/v1/hello"