paint-brush
Pourquoi avez-vous besoin de JWT dans votre projet ASP.NET Core ?par@igorlopushko
7,802 lectures
7,802 lectures

Pourquoi avez-vous besoin de JWT dans votre projet ASP.NET Core ?

par Igor Lopushko16m2024/02/13
Read on Terminal Reader

Trop long; Pour lire

L'histoire explique comment créer une API Web pour générer JWT, puis l'utiliser pour l'autorisation dans l'API Web CRUD.
featured image - Pourquoi avez-vous besoin de JWT dans votre projet ASP.NET Core ?
Igor Lopushko HackerNoon profile picture
0-item

JWT signifie JSON Web Token et il s'agit d'un mécanisme d'autorisation et non d'authentification. Voyons donc quelle est la différence entre ces deux.


L'authentification est le mécanisme qui permet de vérifier que l'utilisateur est exactement celui qu'il prétend être. Il s'agit d'un processus de connexion dans lequel un utilisateur fournit un nom d'utilisateur et un mot de passe, et le système les vérifie. L’authentification répond donc à la question : qui est l’utilisateur ?


L'autorisation est le mécanisme qui permet de vérifier les droits d'accès dont dispose l'utilisateur à une certaine ressource. Il s'agit d'un processus consistant à accorder aux utilisateurs certains rôles et un ensemble d'autorisations dont dispose un rôle particulier. Ainsi, l’autorisation répond à cette question : quels droits l’utilisateur a-t-il dans le système ?

Authentification vs autorisation


Il est important de comprendre que l’authentification vient toujours en premier et l’autorisation en second. En d’autres termes, vous ne pouvez pas obtenir d’autorisation avant d’avoir vérifié votre identité. Mais quelles sont les méthodes d’autorisation les plus populaires ? Il existe deux approches principales pour gérer l'autorisation pour l'application Web.

Séances

Une approche traditionnelle sur le Web pour les utilisateurs autorisés est une session côté serveur basée sur les cookies. Le processus démarre lorsqu'un utilisateur se connecte et qu'un serveur l'authentifie. Après cela, le serveur crée une session avec un ID de session et la stocke quelque part dans la mémoire du serveur. Le serveur renvoie l'ID de session au client et le client stocke l'ID de session dans des cookies. Pour chaque demande, le client envoie un ID de session dans le cadre de la demande, et le serveur vérifie l'ID de session dans sa mémoire et les autorisations de l'utilisateur liées à cette session.

Autorisation basée sur la session

Jetons

Une autre approche populaire consiste à utiliser des jetons pour l'autorisation. Le processus démarre de la même manière lorsqu'un utilisateur saisit son identifiant et ses mots de passe et qu'un client envoie une demande de connexion à un serveur. Au lieu de créer une session, le serveur génère un jeton signé avec le jeton secret. Ensuite, le serveur renvoie le jeton au client et celui-ci doit le stocker dans un stockage local. Semblable à l'approche basée sur la session, le client doit envoyer un jeton au serveur pour chaque requête. Cependant, le serveur ne stocke aucune information supplémentaire sur la session utilisateur. Le serveur doit valider que le token n'a pas changé depuis sa création et sa signature avec la clé secrète.

Autorisation basée sur un jeton

Session vs jeton

L'approche d'autorisation basée sur la session peut être vulnérable à une attaque connue sous le nom de Cross-Site Request Forgery (CSRF). Il s'agit d'une sorte d'attaque lorsque l'attaquant pointe vers un site auquel il est connecté pour effectuer des actions qu'il n'avait pas l'intention de faire, comme soumettre un paiement ou modifier un mot de passe.


Une autre chose est que lorsque vous utilisez une approche d'autorisation basée sur la session, une session avec état est créée entre un client et un serveur. Le problème est que si un client souhaite accéder à différents serveurs dans le cadre de la même application, ces serveurs doivent partager un état de session. Dans un autre cas, il faudra que le client soit autorisé sur chaque serveur puisque la session va être différente.

Partage d'état d'autorisation basé sur la session


D'un autre côté, l'approche d'autorisation basée sur des jetons ne nécessite pas de stocker les données de session côté serveur et peut simplifier l'autorisation entre plusieurs serveurs.


Cependant, les jetons peuvent toujours être volés par un attaquant et il peut également être difficile de les invalider. Nous verrons les détails et comment gérer l'invalidation plus loin dans cet article.

JWT

JSON Web Token (JWT) est un standard ouvert qui définit un moyen compact et autonome de transmettre en toute sécurité des informations entre les parties en tant qu'objet JSON. Ces informations peuvent être vérifiées et fiables car elles sont signées numériquement. Les JWT peuvent être signés à l'aide d'un secret (avec l'algorithme HMAC ) ou d'une paire de clés publique/privée à l'aide de RSA ou ECDSA .

Structure JWT

Les jetons Web JSON se composent de trois parties séparées par des points .


  • Entête
 { "alg": "HS256", "typ": "JWT" }

L'en-tête se compose généralement de deux parties : le type de jeton et l'algorithme de signature utilisé.


  • Charge utile
 { "sub": "1234567890", "name": "John Doe", "admin": true }

La charge utile contient les revendications, qui sont des déclarations sur l'utilisateur. La charge utile est ensuite codée en Base64Url pour former la deuxième partie du jeton Web JSON. Vous pouvez trouver une description des champs standard utilisés comme revendications ici .


  • Signature
 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Pour créer la partie signature, vous devez prendre l'en-tête codé, la charge utile codée, un secret et l'algorithme spécifié dans l'en-tête et le signer.


Le jeton ressemble généralement à ce qui suit :

xxxxx.yyyyy.zzzzz


Vous pouvez accéder à jwt.io et déboguer un exemple de jeton ou le vôtre. Collez simplement votre jeton dans le champ Encodé et sélectionnez l' algorithme de signature du jeton.

débogueur jwt.io

projet .NET

Maintenant que nous avons des connaissances théoriques sur le fonctionnement de JWT, nous pouvons les appliquer au projet réel. Supposons que nous disposions d'une API simple qui représente les opérations CRUD pour l'entité café. Nous allons créer un projet API ASP.NET Core qui représente l'API Coffee. Après cela, nous créerons un autre projet d'API ASP.NET Core qui représenterait une API d'identité pouvant générer JWT. Dans la vraie vie, vous utiliserez probablement Identity Server ou Okta ou Auth0 à des fins d'authentification/autorisation. Cependant, nous créerions notre propre API d'identité pour montrer comment générer JWT. Lorsque l'API Identity est terminée, nous pouvons appeler son contrôleur et générer JWT en fonction des données de l'utilisateur. De plus, nous pouvons protéger l'API Coffee avec une configuration d'autorisation qui nécessite de transmettre JWT à chaque requête.

Paysage du projet .NET

API de café

Tout d’abord, nous allons créer un simple projet d’API ASP.NET Core qui représente l’API Coffee. Voici la structure de ce projet :

API Café - Structure du projet


Commençons par le Coffee.cs dans le dossier Model . Il s’agit d’une entité simple avec des propriétés Id et Name .

 namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }


Nous devons stocker nos entités tout en travaillant avec l'API. Alors, introduisons un simple stockage en mémoire. Il se trouve dans le fichier Storage.cs du dossier Data .

 namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }


Nous avons besoin d'une classe qui représenterait les requêtes adressées à l'API Coffee. Créons donc CoffeeRequest.cs dans le dossier Contracts .

 namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }


Une fois cela fait, nous pouvons implémenter CoffeeController.cs dans le dossier Controller qui représente les opérations CRUD pour l'entité coffee.

 using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }


L'API Coffee est terminée et nous pouvons exécuter le projet et voir l'interface utilisateur de Swagger comme suit :

API Café - Interface utilisateur Swagger

API d'identité

Créons un autre projet d'API ASP.NET Core qui représente l'API d'identité. Voici la structure de ce projet :

API d'identité - Structure du projet

Commençons par le dossier TokenGenerationRequest.cs dans Contracts , qui représente la demande de génération d'un nouveau JWT avec les propriétés Email et Password .

 namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }


Nous devons implémenter uniquement TokenController.cs qui représente la logique de génération JWT. Mais avant de faire cela, le package Microsoft.AspNetCore.Authentication.JwtBearer NuGet doit être installé.

 using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }


Notez que les const sensibles tels que SecretKey , Issuer et Audience doivent être placés quelque part dans la configuration. Ils sont codés en dur uniquement pour simplifier ce projet de test. Le champ Lifetime est défini sur 20 minutes, ce qui signifie que le jeton sera valide pendant cette durée. Vous pouvez également configurer ce paramètre.


Nous pouvons maintenant exécuter le projet et voir l'interface utilisateur de Swagger comme suit :

API d'identité – interface utilisateur Swagger


Appelons le point de terminaison /token et générons un nouveau JWT. Essayez la charge utile suivante :

 { "email": "[email protected]", "password": "password" }


L'API d'identité générera le JWT correspondant :

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM

Activation de l'autorisation dans l'API Coffee

Désormais, lorsque l'API Identity est prête et nous fournit des jetons, nous pouvons garder l'API Coffee avec autorisation. Encore une fois, le package Microsoft.AspNetCore.Authentication.JwtBearer NuGet doit être installé.


Nous devons enregistrer les services requis par les services d'authentification. Ajoutez le code suivant au fichier Program.cs juste après avoir créé un générateur.

 var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();


Il est important de se rappeler que l’ordre dans les middlewares est important. Nous activons l'authentification en appelant la méthode AddAuthentication() et en spécifiant JwtBearerDefaults.AuthenticationScheme comme schéma d'authentification. C'est une constante qui contient une valeur Bearer .

 namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }


Nous devons spécifier TokenValidationParameters qui décrit quels paramètres de JWT seront validés lors de l'autorisation. Nous spécifions également IssuerSigningKey similaire à l'API signingCredentials in Identity pour vérifier la signature JWT. Vérifiez plus de détails sur TokenValidationParameters ici .


Le morceau de code suivant ajoute un middleware au générateur qui active les capacités d'authentification et d'autorisation. Il doit être ajouté entre les méthodes UseHttpsRedirection() et MapControllers() .

 app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();


Maintenant, nous pouvons utiliser l'attribut Authorize sur le contrôleur ou ses actions. En appliquant ce code, toutes les actions dans CoffeeController sont désormais protégées par un mécanisme d'autorisation et JWT doit être envoyé dans le cadre de la demande.

 [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..


Si nous appelons n'importe quel point de terminaison de l'API Coffee, nous pouvons déboguer HttpContext.User et voir qu'il est rempli et possède une Identity avec les revendications que nous avons spécifiées dans JWT. C'est une chose importante pour comprendre comment ASP.NET Core gère l'autorisation sous le capot.

API Coffee – Les réclamations sont renseignées à partir du JWT

Ajouter une autorisation à l'interface utilisateur Swagger

Nous avons fait un excellent travail pour protéger l’API Coffee avec l’autorisation. Mais si vous exécutez le projet Coffee API et ouvrez l'interface utilisateur Swagger, vous ne pourrez pas envoyer JWT dans le cadre de la demande. Pour résoudre ce problème, nous devons mettre à jour le fichier Program.cs avec le code suivant :

 builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });


Après cela, nous pourrons voir le bouton Autoriser dans le coin supérieur droit :

API Coffee - Le bouton Autoriser est apparu


Lorsque vous cliquez sur le bouton Autoriser , vous pourrez saisir JWT comme suit :

API Coffee – Entrez la valeur JWT

Utilisez Postman pour les tests

Vous ne pouvez pas vous limiter à l'utilisation de l'interface utilisateur Swagger et pouvez effectuer des tests de l'API via l'outil Postman. Appelons d'abord le point de terminaison /token de l'API Identity. Nous devons spécifier l'en-tête Content-Type avec la valeur application/json dans la section Headers puisque nous allons utiliser JSON comme charge utile.

API d'identité – Spécifier les en-têtes


Après cela, nous pouvons appeler le point de terminaison /token et obtenir un nouveau JWT.

API d'identité – Générer JWT


Maintenant, nous pouvons copier JWT et l'utiliser pour appeler l'API Coffee. Nous devons spécifier un en-tête Content-Type similaire à l'API Identity si nous souhaitons tester, créer et mettre à jour les points de terminaison. L'en-tête Authorization doit également être défini avec la valeur Bearer [your JWT value] . Après cela, appuyez simplement sur le bouton Envoyer et voyez le résultat.

API Coffee – Obtenir toutes les entités

Autorisation basée sur les rôles

Comme vous vous en souvenez, la partie charge utile de JWT est un ensemble de revendications dont les valeurs sont exactement des paires clé-valeur. L'autorisation basée sur les rôles permet de différencier l'accès aux ressources de l'application en fonction du rôle auquel appartient l'utilisateur.


Si nous mettons à jour la méthode Create() dans le fichier TokenController.cs dans l'API Identity avec le code qui ajoute une nouvelle revendication pour le rôle ; nous pouvons gérer l'authentification basée sur les rôles dans l'API Coffee. ClaimTypes.Role est un nom prédéfini de la revendication de rôle.

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };


Mettez à jour l'attribut Authorize dans le fichier CoffeeController.cs en spécifiant le nom du rôle :

 [Authorize(Roles = "Barista")]


Désormais, tous les utilisateurs qui appellent l’API Coffee doivent revendiquer le rôle avec la valeur Barista . Sinon, ils obtiendront le code de statut 403 Forbidden .

Autorisation basée sur une revendication

Un attribut Authorize peut facilement gérer l’authentification basée sur les rôles. Mais que se passe-t-il si cela ne suffit pas et que nous souhaitons différencier l'accès en fonction de certaines propriétés de l'utilisateur comme l'âge ou autre ? Vous avez probablement déjà deviné que vous pouvez ajouter vos revendications à JWT et les utiliser pour créer une logique d'autorisation. L'autorisation basée sur les rôles elle-même est un cas particulier d'autorisation basée sur les revendications, tout comme un rôle est le même objet de revendication d'un type prédéfini.


Mettons à jour la méthode Create() dans le fichier TokenController.cs dans l'API Identity avec le code qui ajoute une nouvelle revendication IsGourmet .

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };


Dans le fichier Program.cs de l'API Coffee, nous devons créer une stratégie qui vérifie une réclamation et peut être utilisée dans l'attribut Authorize . Le code suivant doit être ajouté juste après l'appel de la méthode AddAuthentication() .

 builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });


Mettez à jour l'attribut Authorize dans le fichier CoffeeController.cs en spécifiant le nom de la stratégie :

 [Authorize(Policy = "OnlyForGourmet")]

Résumé

Toutes nos félicitations! Vous avez fait un gros effort pour apprendre JWT dans .NET. Vous devez désormais avoir une solide compréhension des principes JWT et pourquoi il est important de l'utiliser pour effectuer l'autorisation dans les applications .NET. Mais nous n’avons fait qu’effleurer la surface dans le domaine de l’authentification et de l’autorisation dans les applications ASP.NET Core.


Je suggère de consulter la documentation Microsoft concernant les sujets abordés dans cet article. Il existe également de nombreuses fonctionnalités intégrées pour la gestion des autorisations et des rôles dans la plate-forme .NET. Un bon ajout à cet article pourrait être la documentation Microsoft sur l'autorisation.