Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les jetons JWT, ce qu’un développeur doit connaître

Poster un commentaire

Techniquement un jeton (un token) est simplement une String.
Cette chaîne peut être une suite unique de caractères, mais ce n’est pas le cas le plus intéressant. La plupart du temps les jetons emportent une structure.

On peut mettre beaucoup de choses dans un jeton et donc l’utiliser dans de nombreuses situations. Le principal usage est l’authentification et la gestion des autorisations. Nous allons rester dans ce contexte par la suite. Mais les jetons sont aussi des moyens pratiques pour permettre à des applications d’échanger certaines information de façon sécurisées.

Vous savez qu’il existe deux façons de gérer l’authentification côté serveur d’une application Web.

  1. La création d’un cookie d’authentification
  2. la création d’un jeton d’authentification

 

En pratique nous sommes surtout habitués à manipuler des cookies. Pourquoi changer?

Les jetons apportent une solution plus souple à certaines problématiques, plus particulièrement celle des applications mobiles et de la délégation. C’est donc la voie qui tend de plus en plus à être suivie et en particulier OAuth qui est une norme orientée jeton.

 

JWT (JSON Web Token) est simplement un format de jetons (on prononce comme le mot anglais jot).

Un jeton JWT est une façon de représenter des claims (ou revendication) pour les transférer entre deux parties dans un format compact et compatible uri. Un claim est simplement une collection de paires nom/valeur contenant des informations sur un utilisateur ou tout autre sujet selon l’usage qui est fait du jeton.

Dans un jeton JWT, les claims sont sérialisés en JSON.

Le format d’un jeton JWT est particulièrement bien adapté pour circuler dans une uri et donc pour les applications REST.
JWT est actuellement utilisé par beaucoup de monde et en particulier par Microsoft pour requêter Windows Azure Active Directory via Graph Api. On le rencontre également avec ACS ou le web SSO. Il s’agit également du format standard de OAuth 2 et OpenID Connect.

Comme vous le voyez, en tant que développeur, vous serez amenés à souvent rencontrer JWT. Il est donc important d’avoir quelques lumières à son sujet.

 

Structure d’un jeton JWT

JWT est en cours de normalisation:

http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html

 

Un jeton JWT est une séquence de plusieurs parties compatibles uri séparées par un point (.). Chaque partie est encodée en base64url, c’est à dire un encodage 64 caractères avec un alphabet compatible avec le nom d’une uri.

Le nombre de parties peut varier selon la façon dont le jeton est créé. J’emprunte à Dominick Baier cette image qui montre un exemple de structure JSON et et de structure encodée d’un jeton JWT:

2014-08-25_22-26-12

Les deux parties principales du jeton sont l’entête (header) et les claims.

L’entête est un objet JSON doté des propriétés définies par les normes JWS et JWE selon que l’on encrypte ou bien signe le jeton. Il contient des métas données ainsi que le nom des algorithmes précédents et les clefs nécessaires à ces algorithmes.

 

Les claims contiennent tout ce qui a un sens pour votre application. Libre à vous d’en définir la liste, mais le nom d’un claim doit être unique ou faire partie d’une liste normalisée.

Les noms normalisés sont des noms de claims décrits par la norme. Par exemple le claim exp indique la date d’expiration du jeton.

Ils ne sont pas obligatoires et on n’est pas obligé de tous les utiliser à chaque requête. Disons qu’ils sont souvent utiles. C’est le rôle de l’application qui consomme le jeton de définir ce dont elle a besoin et à quels moments elle en a besoin.

 

Note: Un point intéressant à noter d’ores et déjà, c’est que le jeton d’authentification ne contient pas les accréditations utilisées (credentials). Nous reviendrons là dessus plus loin.

Note: la norme est encore à l’état de préliminaire (draft), mais JWT est suffisamment répandu pour être quasiment certain que ce préliminaire est très proche de la version finale.

 

JWT pour les développeurs

Une propriété importante des claims est d’être compatibles avec les autres formats de credentials. Pour pouvoir convertir en claims un jeton issu d’une autre technologie WIF (Windows Identity Foundation) a introduit la notion de gestionnaire de jeton de sécurité (Security Token Handler). Ce gestionnaire:

  • sérialise et désérialise les jetons
  • valide les jetons de sécurité et en extrait les claims
  • créée un jeton depuis une description de jeton

WIF fournit une classe abstraite SecurityTokenHandler qui sert de base à tout gestionnaire de jeton ainsi qu’un certain nombre de classe dérivées.

2014-08-30_17-45-38

Rien qui concerne JWT, on doit pour cela se tourner vers Nuget qui propose plusieurs packages, par exemple:

http://www.nuget.org/packages/Microsoft.IdentityModel.Tokens.JWT/

https://www.nuget.org/packages/JWT

 

Pour la suite nous utiliserons le JwtSecurityTokenHandler de Microsoft bien que son statut soit peu clair afin de s’intégrer dans le pipeline WIF.

Ceci étant nous n’allons pas configurer le fichier de config pour WIF (on pourrait le faire), l’équipe de dév a fait un gros travail pour facliter la vie avec JWT et fournit des classes et des méthodes qui permettent de manipuler ces jetons dans une application non configurée pour WIF avec beaucoup moins de code que celui d’une application WIF traditionnelle.

 

Créer un jeton

Pour créer un jeton, nous avons besoin d’une clef. Voici un exemple de générateur de clef aléatoire:

static byte[] GetKey()
{
    using (var provider = new RNGCryptoServiceProvider())
    {
        byte[] clefSecrete = new Byte[32];
        provider.GetBytes(clefSecrete);

        return clefSecrete;
    }
}

On pourrait convertir cette clef en String avec:


Convert.ToBase64String(clefSecrete);

 

Essayons maintenant de créer un jeton . Il nous faut tout d’abord un descripteur de jeton:

// création du descripteur de jeton
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor();

// ajouter les claims, par exemple:
Claim[] claims = new Claim[] {
    new Claim(ClaimTypes.Name, "Amethyste"),
    new Claim(ClaimTypes.Role, "Grand chef"),
    new Claim(ClaimTypes.DateOfBirth, "22/02/1964"),
    new Claim(ClaimTypes.Country, "France")
};
tokenDescriptor.Subject = new ClaimsIdentity(claims);

// durée de vie du jeton
DateTime now = DateTime.UtcNow;
tokenDescriptor.Lifetime = new Lifetime(now, now.AddMinutes(1));

// on applique la clef secrète pour signer le jeton.
byte[] clefSecrete = GetKey();
tokenDescriptor.SigningCredentials = new SigningCredentials(
    new InMemorySymmetricSecurityKey(clefSecrete),
             SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest);

// bien sûr dans notre exemple ça pointe vers rien
tokenDescriptor.TokenIssuerName = "http://serveurAutorisation"; // autorité de certification du jeton
tokenDescriptor.AppliesToAddress = "http://localhost:50000/api"; // à qui s'applique le jeton

 

Dans cet exemple, on a signé avec une clef symétrique, c’est à dire une clef partagée entre les parties. La clef servira d’accréditations (credentials).

On a tout ce qu’il faut pour créer le jeton:

JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
string tokenString = tokenHandler.WriteToken(token);

Il va par exemple s’afficher:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJ1bmlxdWVfbmFtZSI6IkFtZXRoeXN0ZSIsInJvbGUiOiJHcmFuZCBjaGVmIiwiYmlydGhkYXRlIjoiMjIvMDIvMTk2NCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2NvdW50cnkiOiJGcmFuY2UiLCJpc3MiOiJodHRwOi8vc2VydmV1ckF1dG9yaXNhdGlvbiIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMDAvYXBpIiwiZXhwIjoxNDA5NDE3OTI5LCJuYmYiOjE0MDk0MTc4Njl9.
K3IO1yu5dMdRlFc4BcTdp4Tx5P5xGQWVu4PrQcAiUcI

On retrouve les 3 parties attendues de notre jeton JWT.

 

On peut désencoder en ligne un jeton sur ce site:

https://developers.google.com/wallet/digital/docs/jwtdecoder

2014-08-30_19-02-07

On retrouve bien les éléments injectés lors de la description (la signature n’est pas affichée sur cette copie d’écran).

 

Note: les jetons JWT ne sont pas cryptés, mais signés. Cela facilite leur examen lors de session de débogage, mais les rend aussi fragiles. Il est donc important de veiller à transférer ces jetons uniquement lors de sessions HTTPS et d’être prudent lors de leur stockage.

 

 Valider un jeton

Dans la vie réelle, on a aussi besoin de valider. J’ai eu un peu de mal pour y arriver, les exemples trouvés sur Internet ne fonctionnent pas, probablement parce que les API ont évoluées. C’est tout le problème, on est dans du préliminaire aussi bien dans la norme que dans les API.

Ceci a fonctionné:

// création des paramètres de validation
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidIssuer = "http://serveurAutorisation";
validationParameters.IssuerSigningToken = new BinarySecretSecurityToken(clefSecrete);
validationParameters.ValidAudience = "http://localhost:50000/api";

try
{
    // on valide
    SecurityToken securityToken;
    ClaimsPrincipal principal = tokenHandler.ValidateToken(tokenString, validationParameters, out securityToken);

    foreach (var claim in principal.Claims)
    {
        Console.WriteLine("Type: {0}, Value: {1}", claim.Type, claim.Value);
    }
}
catch (SecurityTokenException ex)
{
    Console.WriteLine(ex.Message);
}

 

On commence par créer des paramètres de validation puis on invoque la méthode ValidateToken du gestionnaire de jeton et on récupère un SecurityToken dans le paramètre out, tandis que la méthode retourne un ClaimsPrincipal si la validation a réussie.

ClaimPrincipal est une implémentation de IPrincipal qui prend en charge les identités basées sur les claims. Le SecurityToken est bien sûr identique à l’instance obtenue un peu plus haut sur cette ligne:

SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);

 

Nous énumérons ensuite quelques élément du ClaimPrincipal. Un exemple de sortie:

2014-08-30_21-04-38

Les choses peuvent tourner mal, d’où le catch. Si par exemple on utilise une autre clef:

2014-08-30_21-06-57

On est évidement pas authentifié!

 

Parfois on ne cherche pas spécialement à valider, mais à sérialiser/désérialiser un SecurityToken. On dispose pour cela des méthodes WriteToken et ReadToken.

 

Bibliographie

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s