Fonctionnalités incontournables dans ASP.NET Core 2.2 WebApi: Validation

Introduction

La validation des entrées utilisateur est un scénario courant dans une application Web. Pour les applications de production, les développeurs consacrent souvent plus de temps et de code à développer que nous le souhaiterions. Lors de la création d’une WebAPI ASP.NET Core avec FluentValidation, il est important d’essayer de rendre la tâche de validation beaucoup plus facile que par le passé.

FluentValidation est une bibliothèque populaire .NET pour la construction de règles de validation fortement typées.

Configurer le projet

Télécharger le package Nuget FluentValidation.AspNetCore

PM> Install-Package FluentValidation.AspNetCore -Version 8.0.100

Ajouter FluentValidation dans Startup.cs

public void ConfigureServices(IServiceCollection services) 
{ 
   // mvc + validating
   services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddFluentValidation();
}

Créer un validateur

FluentValidation propose plusieurs validateurs intégrés. Dans le prochain exemple, vous verrez comment en utiliser deux parmi toute une panoplie:

  • NotEmpty
  • Must (permet de créer un validateur customisé)

Création d’un modèle a valider

Exemple avec une classe User:

public class User
{
   public string Gender { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string SIN { get; set; }
}

Création du squelette du validateur

Notez que votre validateur doit hériter de la classe abstraite nommée AbstractValidator.

public class UserValidator : AbstractValidator<User>
{
   public UserValidator()
   {
      // Règles ici
   }
}

Création des règles

public static class Utilities
{
   public static bool IsValidSIN(int sin)
   {
      if (sin < 0 || sin > 999999998) return false;

      int checksum = 0;
      for (int i = 4; i != 0; i--)
      {
         checksum += sin % 10;
         sin /= 10;

         int addend = 2 * (sin % 10); if (addend >= 10) addend -= 9;
         checksum += addend;
         sin /= 10;
      }
         return (checksum + sin) % 10 == 0;
   }
}

Dans cet exemple, nous nous assurerons que FirstName, LastName et SIN ne sont ni nuls ni vides, mais également que le numéro SIN est un SIN valide.

Assemblage final des règles de validation

Voici maintenant le résultat final, après assemblage des règles dans la classe de validation:

public class UserValidator : AbstractValidator<User>
{
   public UserValidator()
   {
      RuleFor(x => x.FirstName)
      .NotEmpty()
      .WithMessage("FirstName is mandatory.");

      RuleFor(x => x.LastName)
      .NotEmpty()
      .WithMessage("LastName is mandatory.");

      RuleFor(x => x.SIN)
      .NotEmpty()
      .WithMessage("SIN is mandatory.")
      .Must((o, list, context) =>
      {
         if (null != o.SIN)
         {
            context.MessageFormatter.AppendArgument("SIN", o.SIN);
            return Utilities.IsValidSIN(int.Parse(o.SIN));
         }
         return true;
      })
     .WithMessage("SIN ({SIN}) is not valid.");
   } 
}

Déclaration et activation des règles dans le Startup.cs

public void ConfigureServices(IServiceCollection services) 
{ 
   // Validateurs
   services.AddSingleton<IValidator<User>, UserValidator>();
   // mvc + validatiion
   services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddFluentValidation();
}

Manage your validation errors in Startup.cs

ASP.NET Core 2.2 nous permet de remplacer le comportement par défaut de la gestion de ModelState avec la configuration d’ApiBehaviorOptions. Nous disposons maintenant d’une alternative aux attributs MVC. Dans cet exemple nous configurons ApiBehaviorOptions pour renvoyer au client une réponse de type BadRequestObjectResult (Http 400) avec un objet contenant le code d’erreur, le message d’erreur global et la liste des erreurs rencontrées lors de la validation.

public void ConfigureServices(IServiceCollection services) 
{ 
   // Validators
   services.AddSingleton<IValidator<User>, UserValidator>();
   // mvc + validating
   services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddFluentValidation();

    // override modelstate
    services.Configure<ApiBehaviorOptions>(options =>
    {
       options.InvalidModelStateResponseFactory = (context) =>
       {
          var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => p.ErrorMessage)).ToList();
          var result = new
          {
             Code = "00009",
             Message = "Validation errors",
             Errors = errors
          };
          return new BadRequestObjectResult(result);
       };
    });
}

Quand une validation échoue, cette partie du code est donc exécutée (ModelState est false).

Validation en action!

Nous y voilà! Voyons comment cela fonctionne maintenant avec une démo.

Il est très facile d’utiliser un validateur après sa création.
Il nous suffit de créer votre action qui prend en paramètre votre modèle à valider.
En raison du service de validation ajouté à votre configuration, FluentValidation détectera automatiquement votre modèle lorsque vous l’utiliserez dans une action et activera le validateur!

Exemple de Controller:

[Route("api/[controller]")]
[ApiController]
public class DemoValidationController : ControllerBase
{
   [HttpPost]
   public IActionResult Post(User user)
   {
      return NoContent();
   }
}

Tests avec Postman:

validation-with-fluentValidation-fails

Conclusion

C’était un exemple de la vie courante d’implémentation de la validation dans un WebAPI Core ASP.NET 🙂.
Le code source complet peut être trouvé ici.

Advertisements

Retour sur le MVP Global Summit 2019, Départ de Montréal

Petit souvenir de l’aéroport Montréal Pierre Eliott Trudeau

Ça y est! c’est le grand départ pour le Summit! Nous sommes le dimanche 17 mars 2019!

Départ en groupe avec les autres MVP montréalais (Yannick Plavonil, Guy Barrette, Stephane Lapointe, Christian Hissibini, Vincent Biret)

IMG-1145

Fonctionnalités incontournables dans ASP.NET Core 2.1 WebApi: authentification avec un JWT

Introduction

L’authentification et l’autorisation sont nécessaires pour limiter l’accès aux données personnalisées et / ou sensibles.
Une technique plus sûre que la protection avec des cookies consiste à utiliser des jetons, fournis par un fournisseur de jeton de votre choix.
Dans cet article, nous allons parler des JWT avec OpenID Connect.

OpenID Connect 1.0 est une simple couche d’identité au dessus du protocole OAuth 2.0. Il permet aux clients de vérifier l’identité de l’utilisateur final en fonction de l’authentification effectuée par un serveur d’autorisations, ainsi que d’obtenir des informations de profil de base sur l’utilisateur final de manière interopérable et de type REST. OpenID Connect permet aux clients de tous types, y compris les clients Web, mobiles et JavaScript, de demander et de recevoir des informations sur les sessions authentifiées et les utilisateurs finaux. La suite de spécifications est extensible, permettant aux participants d’utiliser des fonctionnalités optionnelles telles que le cryptage des données d’identité, la découverte des fournisseurs OpenID et la gestion de session, lorsque cela leur semble judicieux.

Pour plus d’informations sur la configuration d’un fournisseur Open Id Connect, vous pouvez consulter cet exemple avec Azure AD ici.

Configurer le Startup.cs

Etape 1: Télécharger le package Microsoft.AspNetCore.Authentication.JwtBearer Nuget

PM> Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 2.1.2

Etape 2: Paramétrer le schéma d’authentification avec un JWT

public void ConfigureServices(IServiceCollection services)
{
   // Authentication
   services.AddAuthentication(options =>
   {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
   });
}

Etape 3: Configurer le fournisseur de jeton JWT

public void ConfigureServices(IServiceCollection services)
{
   // Authentication
   services.AddAuthentication(options =>    {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
   }).AddJwtBearer(options =>
   {
      options.Authority = "provider end point";
      options.Audience = "application id or uri as identifier";
      options.TokenValidationParameters.ValidateLifetime = true;
      options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
   });
}

Audience représente le destinataire prévu du jeton entrant ou de la ressource à laquelle le jeton accorde l’accès. Si la valeur spécifiée dans ce paramètre ne correspond pas au paramètre ‘aud’ dans le jeton, celui-ci sera rejeté car il était destiné à être utilisé pour accéder à une ressource différente. Notez que différents fournisseurs de jetons de sécurité ont des comportements différents concernant ce qui est utilisé comme revendication ‘aud’ (certains utilisent l’URI d’une ressource à laquelle un utilisateur souhaite accéder, d’autres utilisent des noms de portée). Assurez-vous d’utiliser une Audience qui a du sens compte tenu des jetons que vous prévoyez d’accepter.

Autority est l’adresse du serveur d’authentification émettant des jetons. Le middleware d’authentification du support JWT utilisera cet URI pour rechercher et récupérer la clé publique pouvant être utilisée pour valider la signature du jeton. Il confirmera également que le paramètre ‘iss’ du jeton correspond à cet URI.

ValidateLifetime valide l’expiration du jeton.

ClockSkew autorise une certaine dérive d’horloge. Je recommande 5 minutes ou moins.

Etape 4: Paramétrer les policies d’autorisation

services.AddAuthorization(opts =>
{
   opts.AddPolicy("SurveyCreator", p =>
   {
      // Using value text for demo show, else use enum : ClaimTypes.Role
      p.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "SurveyCreator");
   });
});

Cette policy nécessite un Claim de type ‘role’ avec la valeur «SurveyCreator».

Etape 5: Protéger vos actions avec l’attribut Authorize

[Authorize(Policy = "SurveyCreator")]
[HttpPost]
public void Post([FromBody] string value)
{
}

Rappelez-vous que si vous ne fournissez pas un jeton à votre Api ou un jeton erroné ou expiré, la réponse sera Http Unauthorized.

Si votre jeton ne satisfait pas à la policy, vous obtiendrez Http Forbidden.

Conclusion

C’était un exemple d’implémentation de la sécurité dans une WebAPI 🙂

Le code source complet peut être trouvé ici.