← Tous les articles

Confiance distribuée avec l'authentification de service à service

Sur cette page

Dans une architecture orientée services (SOA), les « services » sont les briques de base de ton application. Un service est autonome, faiblement couplé et sans état. Autonome, parce qu’il ne dépend ni de l’état ni du contexte des autres services. Faiblement couplé, parce qu’il ne dépend pas des technologies des autres. Sans état, parce qu’il ne garde rien en mémoire d’une requête à l’autre.

Les services sont les briques de ton application. Ils sont aussi les briques de ton architecture de sécurité.

Distribuer la confiance

Dans une SOA, les services sont indépendants les uns des autres. Ils sont donc aussi indépendants des mécanismes de sécurité des autres services.

C’est une bonne chose.

Tu peux utiliser des mécanismes de sécurité différents d’un service à l’autre. Des mécanismes d’authentification différents, des mécanismes d’autorisation différents, des mécanismes de chiffrement différents, etc.

C’est aussi une mauvaise chose.

Tu dois gérer des mécanismes de sécurité différents d’un service à l’autre. Des mécanismes d’authentification différents, des mécanismes d’autorisation différents, des mécanismes de chiffrement différents, etc.

Authentification de service à service

Dans une SOA, les services sont indépendants les uns des autres. Tu vois le motif.

Authentifier les requêtes entre services pose problème. Tu ne peux pas réutiliser les mécanismes qui authentifient les utilisateurs. Pas de cookies, pas de sessions, et surtout, tu ne veux pas dépendre d’un fournisseur d’identité (IdP).

Avec les offres de plateforme en tant que service (PaaS) comme Cloudflare Workers, Heroku, Cloud Foundry et Vercel, tu ne peux même pas compter sur l’infrastructure sous-jacente pour t’offrir un canal sûr entre les services.

Il te faut un moyen d’authentifier les requêtes entre services qui soit indépendant de l’infrastructure sous-jacente et des mécanismes de sécurité des autres services.

C’est là qu’entrent en scène les JWT et les JWKS.

JSON Web Tokens (JWT)

Les JSON Web Tokens (JWT) sont un moyen compact, sûr dans une URL, de représenter des claims échangés entre deux parties. Les claims d’un JWT sont encodés dans un objet JSON qui sert de charge utile (payload) à une structure JSON Web Signature (JWS), ou de texte en clair à une structure JSON Web Encryption (JWE). Les claims peuvent ainsi être signés numériquement, protégés en intégrité par un code d’authentification de message (MAC), et/ou chiffrés.

Structure d’un JWT

Un JWT est une chaîne en trois parties : l’en-tête (header), la charge utile (payload) et la signature. L’en-tête et la charge utile sont des objets JSON. La signature est une chaîne encodée en Base64.

<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>

En-tête du JWT

L’en-tête compte en général deux éléments : le type du token, soit JWT, et l’algorithme de signature utilisé, comme HMAC SHA256 ou RSA.

{
  "alg": "HS256",
  "typ": "JWT"
}

Charge utile du JWT

La charge utile contient les claims. Les claims sont des déclarations sur une entité (en général l’utilisateur) et des métadonnées supplémentaires. Il en existe trois types : enregistrés, publics et privés.

  • Claims enregistrés : un ensemble de claims prédéfinis, non obligatoires mais recommandés, pour offrir des claims utiles et interopérables. Parmi eux : iss (émetteur), exp (date d’expiration), sub (sujet), aud (audience), et d’autres.
  • Claims publics : ceux qui les utilisent les définissent à leur guise. Pour éviter les collisions, il faut les déclarer dans le registre IANA des JSON Web Token, ou les définir comme une URI qui contient un espace de noms à l’abri des collisions.
  • Claims privés : des claims sur mesure, créés pour partager de l’information entre des parties qui ont convenu de les utiliser.
{
  "sub": "1234567890",
  "iss": "https://my-experience-api.willhackett.xyz",
  "aud": "https://my-resource-api.willhackett.xyz"
}

Les claims iss et aud identifient respectivement l’émetteur et l’audience du JWT. Le claim sub identifie le sujet du JWT. Ici, le sujet est l’ID de l’utilisateur pour le compte duquel la requête est faite. Dans un cas réel, je te conseille de remballer le JWT de l’utilisateur qui fait la requête dans le nouveau JWT. Le service destinataire peut alors vérifier l’identité de l’utilisateur, et vérifier qu’il a les bonnes permissions pour effectuer l’action demandée dans ta couche de ressources.

L’émetteur et l’audience reviendront plus loin, quand je parlerai des frontières de confiance.

Signature du JWT

Pour créer la signature, tu prends l’en-tête encodé, la charge utile encodée, un secret et l’algorithme indiqué dans l’en-tête, puis tu signes le tout.

HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

La signature sert à vérifier que le message n’a pas été modifié en route. Pour les tokens signés avec une clé privée, elle vérifie aussi que l’expéditeur du JWT est bien celui qu’il prétend être.

JSON Web Key Sets (JWKS)

Les JSON Web Key Sets (JWKS) sont des ensembles de clés qui contiennent les clés publiques servant à vérifier tout JSON Web Token (JWT) émis par le serveur d’autorisation et signé avec l’algorithme RS256.

Structure d’un JWKS

Un JWKS est un objet JSON qui représente un ensemble de JWK. L’objet JSON DOIT avoir un membre « keys », un tableau de JWK. C’est le format JWK Set.

{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
      "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
      "use": "enc",
      "kid": "1"
    },
    {
      "kty": "RSA",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFb.....<remainder of RSA key omitted>.....ZU3e_TURo9-F1bp-hqOY_BLPbEx-Rmc6XaXk",
      "e": "AQAB",
      "alg": "RS256",
      "kid": "2011-04-29"
    }
  ]
}

Chez nous, seules les clés publiques nous intéressent. Elles servent à vérifier la signature des JWT.

Modèle de confiance

Le modèle de confiance est simple. Le service qui émet le JWT est censé émettre des JWT valides. Le service qui reçoit le JWT est censé le vérifier. Le service qui reçoit le JWT n’a pas le droit d’émettre des JWT valides au nom du service appelant.

Si le service A émet un JWT à destination du service B, on fait confiance au service B pour vérifier le JWT avec le JWKS fourni par le service A. Le service B ne peut pas signer un JWT et s’attendre à ce que le service C lui fasse confiance. C’est là que les frontières de confiance entrent en jeu.

Frontières de confiance

Les frontières de confiance séparent les services qui se font confiance.

If Service A issues a JWT to Service B, Service B is trusted to verify the JWT against the JWKS provided by Service A.

Chaque JWT contient un claim iss et un claim aud. L’aud est l’audience visée par le JWT, et sert à vérifier que la requête est bien destinée au bon service. L’iss est l’émetteur du JWT, et sert à vérifier que le JWT vient d’un service de confiance.

Le service B fait confiance au service A pour émettre des JWT valides. Si le nom d’hôte du service A est service-a.willhackett.xyz et celui du service B service-b.willhackett.xyz, le claim d’émetteur sera https://service-a.willhackett.xyz et le claim d’audience https://service-b.willhackett.xyz.

Le service B accepte les tokens émis avec le claim d’émetteur https://service-a.willhackett.xyz et le claim d’audience https://service-b.willhackett.xyz. Il appelle l’endpoint JWKS du service A pour récupérer les clés publiques qui vérifient les JWT émis par le service A.

Service mesh

La question se pose : pourquoi pas un service mesh ? Les service meshes sont très bien, mais ils ne règlent pas tout. Ils ne conviennent malheureusement pas à tous les cas. Plus développer pour le web revient à développer pour l’edge, ou sur une plateforme, moins les service meshes servent.

À l’intérieur de ton infrastructure, un service mesh est une excellente solution. Déployer des conteneurs avec un proxy sidecar est une très bonne façon de standardiser la communication de service à service. Mais que fais-tu quand tu déploies sur une plateforme comme Cloudflare Workers, Heroku, Cloud Foundry ou Vercel ?

Avec les plateformes, tu dois t’appuyer sur des standards.

Autres réflexions

Avec ce modèle de confiance distribuée, tu dois t’assurer que les clés RSA générées par ton runtime restent cohérentes quand tu passes à l’échelle horizontalement. Comme ça, l’appel JWKS renvoie toujours les mêmes clés publiques.

Ce n’est qu’un standard parmi d’autres pour l’authentification HTTP de service à service. Tu peux aussi miser sur le TLS mutuel (mTLS), des secrets partagés, etc. L’essentiel, c’est d’avoir un standard indépendant de l’infrastructure sous-jacente et des mécanismes de sécurité des autres services.

Références