Lorsque l’on utilise le Type d’Autorisation JWT Bearer (JWT Bearer Authorization Grant) dans le cadre de OAuth 2.0, ou encore dans le cadre du protocole OpenID Connect, une application peut obtenir un jeton d’accès (Access Token) au format JWT, en parallèle avec le jeton opaque normal.

Le jeton lie l’identité de l’application, l’identité du serveur et celle de l’utilisateur final de façon fiable car le JWT est signé numériquement.

Après avoir décrit le jeton JWT, nous indiquons comment le vérifier et le consommer.

Voyez ici ce que l’on peut faire avec : Un jeton d’identification peut être utilisé pour ....

JSON Web Token (JWT)

, par DnC

JWT est un standard ouvert (RFC 7519) qui définit une manière compacte et autonome de transmission sécurisée d’informations entre les parties sous la forme d’un objet JSON.

Les documents suivants spécifient comment le jeton JWT est mis en oeuvre :
- pour OAuth 2.0 : RFC 1523
- pour OpenID Connect : OpenID Connect Core 1.0 incorporating errata set 1.

Structure du Jeton JWT

Un jeton JWT se compose de trois parties séparées par des points (.) :

<header>.<payload>.<signature>

Chacune de ces trois composantes est codé Base64Url. OAuth 2.0 Server PHP utilise les fonctions suivantes :

PHP

  1. /**
  2. * @author Brent Shaffer <bshafs at gmail dot com>
  3.  * @license MIT License
  4. */
  5.  
  6. public function urlSafeB64Encode($data)
  7. {
  8. $b64 = base64_encode($data);
  9. $b64 = str_replace(array('+', '/', "\r", "\n", '='),
  10. array('-', '_'),
  11. $b64);
  12.  
  13. return $b64;
  14. }
  15.  
  16. public function urlSafeB64Decode($b64)
  17. {
  18. $b64 = str_replace(array('-', '_'),
  19. array('+', '/'),
  20. $b64);
  21.  
  22. return base64_decode($b64);
  23. }

Télécharger

Bien que son format compact incite à le transmettre par URL, il est recommandé de le transmettre par Header dans l’en-tête Authorization avec le mécanisme d’authentification Bearer :

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImRlbW8iLCJmdWxsTmFtZSI6

Header : Entête

L’en-tête se compose généralement d’un array JSON de deux membres :

Alg : le type du jeton, qui est donc JWT
Typ : l’algorithme de hachage utilisé, comme HMAC SHA256 ou RSA.

Pour coder/décoder la signature, OAuth Server by DnC utilise par défaut l’algorithme HS256 et peut utiliser HS384 ou HS512 ainsi que RS256, RS384 et RS512.

Par exemple :

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

Paylod : Charge utile

La charge utile contient les déclarations sous le format JSON.

Les déclarations (claims) sont des informations sur une entité (généralement, l’utilisateur) et des métadonnées supplémentaires.

Il existe trois types de déclarations : reserved, public et private.

Déclarations réservées

Ces déclarations sont réservées, en ce sens que leur nom ne doit pas être utilisé par d’autres déclarations qui pourraient être définies dans le cadre d’un protocole ou d’une application particuliers.

Elles sont prédéfinies par le standard JWT (voir Registered Claim Names).

Format des déclarations réservées

Les déclarations réservées propres à JWT sont les suivantes (voir JWT Format and Processing Requirements) :

1. Le jeton JWT DOIT contenir une déclaration "iss" (émetteur) contenant l’identifiant unique de l’entité qui a émis le jeton.

2. Le jeton JWT DOIT contenir une déclaration "sub" (subject) identifiant le Principal qui est le sujet de la JWT. Deux cas doivent être différenciés :

A. Pour Authorization Grant, le sujet identifie généralement un accesseur autorisé pour lequel le jeton d’accès est demandé (c’est-à-dire le propriétaire de la ressource ou un délégué autorisé), mais dans certains cas, ce peut être un identifiant pseudonyme ou une autre valeur indiquant un utilisateur anonyme. [1]

B. Pour l’authentification client, le sujet DOIT être le "Client_id" du client OAuth.

3. Le jeton JWT DOIT contenir une définition "aud" (audience), audience(s) pour laquelle (ou lesquelles)ce jeton d’identité est destiné. Il DOIT contenir le client_id OAuth 2.0 de la partie de confiance en tant que valeur d’audience. Il PEUT aussi contenir des identifiants pour d’autres audiences. Dans le cas général, la valeur aud est un tableau de chaînes sensibles à la casse. Dans le cas spécial commun où il y a un public, la valeur aud PEUT être une chaîne sensible à la casse.

4. Le jeton JWT DOIT contenir une définition "exp" (expiration time) qui limite la fenêtre de temps pendant laquelle le JWT peut être utilisé.

5. Le JWT PEUT contenir une définition « nbf » (not before) qui identifie l’instant avant lequel le jeton NE DOIT PAS être accepté.

6. Le JWT PEUT contenir une définition "iat" (issued at) qui identifie l’instant auquel le jeton a été délivré.

7. Le jeton JWT PEUT contenir une définition "jti" (JWT ID) qui fournit un identifiant unique pour le jeton. Si c’est le cas, le serveur OAuth Server by DnC s’assure qu’un jeton JWT n’est pas réutilisé pendant sa durée de validité.

Le jeton JWT peut également présenter une déclaration at_hash, voir : Validation du jeton d’accès avec la déclaration at_hash du jeton d’identité.

Déclarations publiques

Elles peuvent être définies à volonté. Mais pour éviter les collisions, leurs noms devraient être choisis dans le IANA JSON Web Token Registry mentionné précédemment ou être définis avec la déclaration d’un espace de noms.

Déclarations privées

Ce sont des déclarations personnalisées créées pour partager des informations entre les parties qui ont prévu de les utiliser.

Exemple de déclarations :

{  
 "iss": "https:\/\/oa.dnc.global",
 "iat":1410255630,
 "exp":1410259230,
 "sub": "a394190a00f2d36b21309036b0b",
 "aud":"_c036b0b6f97a394190a00f2d36b213091938d225"
}

Signature

La signature est utilisée pour vérifier que l’expéditeur du jeton JWT est bien celui qu’il prétend être et de veiller à ce que le message n’a pas été modifié en cours de route.

Le JWT est signé en utilisant un secret (avec l’algorithme HMAC) ou une paire de clés publique/privée en utilisant RSA.

C’est cette deuxième solution qui est définie par OpenID Connect (la signature par HMAC est jugée insuffisamment sécurisée). Lorsque l’application passe le jeton à un serveur de ressource protégée (RS), il est impératif que le serveur de ressource valide le jeton. Le serveur de ressource (RS) qui valide la signature doit être en possession de la clé publique correspondant à la clé privée avec laquelle la signature a été élaborée. Il est également possible, voire avantageux, de faire valider le jeton par le serveur d’authentification (introspection).

La paire de clés publique/privée doit avoir été définie par l’auteur au moment de l’inscription de l’application cliente sur le serveur OAuthSD.

Notes

[1à faire : rédiger sous une forme plus compréhensible et plus proche du vocabulaire utilisé par ailleurs.

Validation du jeton JWT

, par DnC

Problématique

Le jeton d’identité, de type JWT, doit être validé dans deux situations :

- Dès sa réception, conformément à ce qui est défini dans la spécification d’OpenID Connect [1].

- Pour autoriser l’accès à une ressource protégée. Voyez comment le problème se pose de façon générale : Validation du jeton d’accès par une ressource protégée.
Deux cas se présentent :
- soit on passe la clé publique au serveur de ressource protégée RS qui procède localement à sa validation ; si ce RS n’est pas lié à l’organisation qui contrôle le serveur d’authentification, il peut utiliser la fonction OpenID Connect : Découverte,
- soit on utilise une méthode dite "introspection" consistant à demander l’authentification du jeton JWT au serveur d’authentification qui l’a délivré : Open ID Connect : Introspection .

Code pour la Validation et la Consommation du jeton JWT

La validation d’un jeton d’identification nécessite plusieurs étapes. Que le jeton soit validé du côté du serveur d’authentification ou à distance, la méthode est la même.

Le décodage et la validation du jeton suivent la méthode définie ici : Spécification OpenID Connect : Validation du jeton d’identité :

Les clients DOIVENT valider le jeton ID dans la réponse au jeton de la manière suivante :

- Si le jeton d’identification est chiffré, déchiffrez-le à l’aide des clés et des algorithmes spécifiés lors de l’enregistrement par le client, que l’OP devait utiliser pour chiffrer le jeton d’identification. Si le chiffrement a été négocié avec l’OP au moment de l’enregistrement et que le jeton d’identification n’est pas chiffré, le RP [2] DEVRAIT le rejeter.
- L’identifiant de l’émetteur pour le fournisseur OpenID (qui est généralement obtenu lors de la découverte) DOIT correspondre exactement à la valeur de la déclaration iss (issuer).
- Le client DOIT valider que la déclaration aud (audience) contienne la valeur client_id enregistrée auprès de l’émetteur identifié par la déclaration iss (émetteur) en tant qu’audience. La déclaration aud (audience) PEUT contenir un tableau avec plus d’un élément. Le jeton ID DOIT être rejeté si le jeton ID ne répertorie pas le client en tant qu’audience valide, ou s’il contient des audiences supplémentaires non approuvées par le client.
- Si le jeton d’identification contient plusieurs audiences, le client DEVRAIT vérifier qu’une déclaration azp est présente.
- Si une déclaration azp (partie autorisée) est présente, le client DEVRAIT vérifier que son client_id est la valeur de la déclaration.
- Si le jeton ID est reçu via une communication directe entre le client et le point d’extrémité du jeton (qui se trouve dans ce flux), la validation TLS du serveur PEUT être utilisée pour valider l’émetteur au lieu de vérifier la signature du jeton [3] . Le client DOIT valider la signature de tous les autres jetons ID conformément à JWS [JWS] en utilisant l’algorithme spécifié dans le paramètre d’en-tête JWT alg. Le client DOIT utiliser les clés fournies par l’émetteur.
- La valeur alg DEVRAIT être la valeur par défaut de RS256 ou l’algorithme envoyé par le client dans le paramètre id_token_signed_response_alg lors de l’enregistrement.
- Si le paramètre d’en-tête JWT alg utilise un algorithme basé sur MAC, tel que HS256, HS384 ou HS512, les octets de la représentation UTF-8 du secret client correspondant à l’identifiant client contenu dans la réclamation aud (audience) sont utilisés comme clé de validation de la signature. Pour les algorithmes basés sur MAC, le comportement n’est pas spécifié si l’aud est multivalué ou si une valeur azp est différente de la valeur aud.
- L’heure actuelle DOIT être antérieure à l’heure représentée par la déclaration exp.
- La déclaration Iat peut être utilisé pour rejeter des jetons qui ont été émis trop loin de l’heure actuelle, limitant ainsi la durée de stockage des clés pour prévenir les attaques. La plage acceptable est spécifique au client.
- Si une valeur nonce a été envoyée dans la demande d’authentification, une déclaration de nonce DOIT être présente et sa valeur vérifiée pour contrôler qu’il s’agit de la même valeur que celle qui a été envoyée dans la demande d’authentification. Le client DEVRAIT vérifier la valeur de nonce pour les attaques par relecture. La méthode précise pour détecter les attaques par relecture est spécifique au client.
- Si la déclaration acr a été mentionnée, le client DEVRAIT vérifier que la valeur de réclamation revendiquée est appropriée. La signification et le traitement des déclarations acr sont hors du domaine d’application de la présente spécification.
- Si la déclaration auth_time a été mentionnée, par le biais d’une demande spécifique pour cette déclaration ou du paramètre max_age, le client DEVRAIT vérifier la valeur de la déclaration auth_time et demander une nouvelle authentification s’il détermine qu’il s’est écoulé trop de temps depuis la dernière authentification de l’utilisateur final.

La plupart de ces vérifications ne nécessitent qu’une simple comparaison de chaînes. La validation de la signature est plus complexe. L’implémentation qui en est faite par OAuthSD est décrite maintenant. La fonction suivante, tirée de OAuth 2.0 Server PHP, sépare les composantes du jeton, détecte les erreurs de format, vérifie éventuellement la signature et retourne la charge utile ou false en cas d’erreur.

La variable $key passe la clé publique qui a servi à générer le jeton JWT.

Si $key = null, la vérification de la signature n’est pas effectuée (on suppose le jeton déjà validé par introspection), et le contenu de la charge utile est retourné sous la forme d’un tableau associatif.

PHP

  1. /**
  2. * @author Brent Shaffer <bshafs at gmail dot com>
  3.  * @license MIT License
  4. */
  5.  
  6. /**
  7.   * Sépare les composantes du jeton, détecte les erreurs de format, vérifie la signature et retourne la charge utile ou false en cas d'erreur.
  8.   *
  9.   * @param mixed $jwt : le jeton JWT
  10.   * @param mixed $key : la clé publique
  11.   * @param mixed $allowedAlgorithms : un array des codes d'algorithmes autorisés (sous ensemble de HS256, HS384 ou HS512, RS256, RS384 et RS512). Si ce paramètre est précisé, le jeton doit indiquer l'algorithme et celui-ci doit être compris dans l'array.
  12.   * @param mixed return : charge utile (tableau associatif) ou false.
  13.   */
  14.  
  15. function decode($jwt, $key = null, $allowedAlgorithms = true)
  16. {
  17. if (!strpos($jwt, '.')) {
  18. return false;
  19. }
  20.  
  21. $tks = explode('.', $jwt);
  22.  
  23. if (count($tks) != 3) {
  24. return false;
  25. }
  26.  
  27. list($headb64, $payloadb64, $cryptob64) = $tks;
  28.  
  29. if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) {
  30. return false;
  31. }
  32.  
  33. if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) {
  34. return false;
  35. }
  36.  
  37. $sig = $this->urlSafeB64Decode($cryptob64);
  38.  
  39. if ((bool) $allowedAlgorithms) {
  40. if (!isset($header['alg'])) {
  41. return false;
  42. }
  43.  
  44. // check if bool arg supplied here to maintain BC
  45. if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) {
  46. return false;
  47. }
  48.  
  49. if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) {
  50. return false;
  51. }
  52. }
  53.  
  54. return $payload;
  55. }
  56.  
  57. function verifySignature($signature, $input, $key, $algo = 'HS256')
  58. {
  59. // use constants when possible, for HipHop support
  60. switch ($algo) {
  61. case'HS256':
  62. case'HS384':
  63. case'HS512':
  64. return $this->hash_equals(
  65. $this->sign($input, $key, $algo),
  66. $signature
  67. );
  68.  
  69. case 'RS256':
  70. return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1;
  71.  
  72. case 'RS384':
  73. return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1;
  74.  
  75. case 'RS512':
  76. return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1;
  77.  
  78. default:
  79. throw new \InvalidArgumentException("Unsupported or invalid signing algorithm.");
  80. }
  81. }

Télécharger

Pour en savoir plus sur la validation du JWT :

- Open ID Connect : Introspection
- OpenId Connect : Point d’extrémité d’informations sur les clefs (Keys Endpoint)
- OpenID Connect : Exemples complets du flux d’Autorisation via un code puis requête UserInfo

Notes

[1Voici pourtant ce que dit Google : "Normalement, il est essentiel de valider un jeton d’identification avant de l’utiliser, mais puisque vous communiquez directement avec Google via un canal HTTPS sans intermédiaire et que vous utilisez le secret de votre client pour vous authentifier auprès de Google, vous pouvez être sûr que le jeton que vous recevez vient vraiment de Google et est valide. Si votre serveur transmet le jeton d’identification à d’autres composants de votre application, il est extrêmement important que les autres composants le valident avant de l’utiliser. " (https://developers.google.com/ident...).

[2Relying Party : l’application cliente ou le serveur de ressource protégée etc..

[3Autrement dit, si la liaison entre le client et le serveur est sécurisée par TLS, on pourrait se passer de valider la signature. C’est ce que dit Google ici : https://developers.google.com/ident... . Cependant, nous considérons qu’il faut toujours valider la signature du jeton quel que soit l’utilisation, et pas seulement dans le cas où le jeton est retransmis à une application ou ressource tierce.

Nonce

, par DnC

Le paramètre nonce lie le jeton d’identification JWT avec le client. OAuthSD présente une façon de générer le nonce et de le valider propre à modérer les attaques tentant de rejouer un jeton intercepté au profit d’une application malicieuse.

Spécifications OpenID Connect :

nonce - Valeur de chaîne utilisée pour associer une session Client à un ID de jeton et pour réduire les attaques par relecture. La valeur est transmise non modifiée de la demande d’authentification au jeton d’ID. S’ils sont présents dans le jeton d’ID, les clients DOIVENT vérifier que la demande est égale à la valeur du paramètre nonce dans la demande d’authentification. S’ils sont présents dans la demande d’authentification, les serveurs d’autorisation DOIVENT inclure une déclaration dans le jeton d’identification, la valeur déclarée étant la valeur de nonce de la demande d’authentification. Les serveurs d’autorisation NE DEVRAIENT effectuer aucun autre traitement sur les valeurs nonce utilisées. La valeur nonce est une chaîne sensible à la casse.

nonce et state

state (défini pour le protocole Oauth 2.0) et nonce (défini pour OpenID Connect) semblent similaires. Sans entrer dans les détails de state, notons que OAuth 2 limite cette déclaration à l’obtention du jeton d’autorisation, tandis que le paramètre nonce est étendu au jeton d’identité.

La spécification excluant tout traitement sur nonce, OAuthSD est transparent au nonce : le nonce est vérifié mais non modifié, donc retourné tel qu’il a été envoyé par l’application cliente. Vu de l’application cliente, le nonce est similaire à un jeton opaque. En ce sens, nonce présente une similitude avec state.

Mise en oeuvre de nonce par OAuthSD

OAuthSD [1], dans les flux OpenID Connect, met en oeuvre nonce à l’initiative de l’application cliente.

Le paramètre nonce est obligatoire pour les types de réponse "id_token" et "id_token token" (flux implicite et flux hybride). Cependant, DnC recommande l’utilisation du nonce dans tous les flux.

La façon de générer le nonce et de le valider n’est pas définie par le standard. Le serveur doit être transparent au nonce. L’élaboration et la vérification de sa valeur sont donc laissées à l’initiative du développeur de l’application cliente.
Pour autant, le nonce est-il une valeur aléatoire quelconque ?
Les plugins pour applications développés par DnC calculent un nonce fondé sur des données caractéristiques de l’application et de son environnement.

La fonction d’introspection implémentée par OauthSD pour OpenID Connect assure la validation de ce type de nonce.

Tel qu’est définie la valeur de nonce dans le cadre d’OAuthSD, il devient possible de la valider à différentes étapes internes au serveur d’authentification, pas seulement au retour sur l’application cliente.

Ainsi, une attaque de type man in the middle qui tenterait d’utiliser un jeton d’identité (JWT) au profit d’une application étrangère est vouée à l’échec.

Notes

[1qui s’appuie en cela sur la bibliothèque de Brent Shaffer