ASP .NET Core MVC – Sécuriser vos services REST avec JWT

Introduction
Dans les applications ASP .NET Core MVC, Web API permet de créer des services REST, permettant à des applications distantes d’accéder et de gérer des données. Avant de déployer ces services, il est nécessaire de les sécuriser afin que seuls les utilisateurs autorisés puissent y accéder.
JWT (JSON Web Tokens) est une solution permettant de sécuriser l’accès à ces services. Le principe est de consommer un service d’authentification à partir d’informations d’identification (nom d’utilisateur, mot de passe, …). Si l’authentification réussie, alors un jeton est communiqué. L’application devra alors communiquer ce jeton dans les requêtes HTTP envoyées pour consommer le service REST.

Mise en œuvre

Pour réaliser le cryptage des données, commençons par définir une clé dans le fichier appsettings.json :
{
  "Parametres": {
    "Cle": "F25073BA-68BB-4CD7-82CE-2339C45BAA9F"
  }
}

Pour faciliter l’accès à ce paramètre, implémentons une classe contenant une propriété portant le même nom que le paramètre défini ci-dessus :

public class Parametres
{
    public string Cle { get; set; }
}

La classe ci-dessous permet de définir les caractéristiques des utilisateurs et d’en créer :

public class Utilisateur
{
    public int Id { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string NomUtilisateur { get; set; }
    public string MotDePasse { get; set; }
    public string Jeton { get; set; }

    public Utilisateur()
    {
        this.Id = -1;
        this.Nom = string.Empty;
        this.Prenom = string.Empty;
        this.NomUtilisateur = string.Empty;
        this.MotDePasse = string.Empty;
        this.Jeton = string.Empty;
    }

    public Utilisateur(int aId, string aNom, string aPrenom, string aNomUtilisateur, string aMotDePasse)
        : this()
    {
        this.Id = aId;
        this.Nom = aNom;
        this.Prenom = aPrenom;
        this.NomUtilisateur = aNomUtilisateur;
        this.MotDePasse = aMotDePasse;
    }
}

Voici une classe avec son interface permettant de définir et d’authentifier les utilisateurs autorisés à consommer le service REST. Dans cet exemple, les utilisateurs sont définis directement dans le code :

public interface IUtilisateurService
{
    Utilisateur Authentifier(string aNomUtilisateur, string aMotDePasse);
}

public class UtilisateurService : IUtilisateurService
{
    private Parametres Parametres { get; }

    // Liste des utilisateurs
    private List<Utilisateur> ListeUtilisateurs
    {
        get => new List<Utilisateur>() { new Utilisateur(1, "RAVAILLE", "James", "algo", "pass") };
    }

    public UtilisateurService(IOptions<Parametres> appSettings)
    {
        this.Parametres = appSettings.Value;
    }

    public Utilisateur Authentifier(string aNomUtilisateur, string aMotDePasse)
    {
        // Variables locales.
        Utilisateur utilisateur;
        JwtSecurityTokenHandler tokenHandler;
        SecurityTokenDescriptor tokenDescriptor;
        SecurityToken token;
        byte[] key;

        // Recherche de l'utilisateur.
        utilisateur = this.ListeUtilisateurs.SingleOrDefault(x => x.NomUtilisateur == aNomUtilisateur && x.MotDePasse == aMotDePasse);
        if (utilisateur == null)
            return null;

        // Génération du jeton.
        tokenHandler = new JwtSecurityTokenHandler();
        key = Encoding.ASCII.GetBytes(this.Parametres.Cle);
        tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.Name, utilisateur.Id.ToString())
            }),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        token = tokenHandler.CreateToken(tokenDescriptor);
        utilisateur.Jeton = tokenHandler.WriteToken(token);

        // Effacement du mot de passe (raison de sécurité).
        utilisateur.MotDePasse = null;

        // Retour.
        return utilisateur;
    }
}

La méthode Authentifier permet de générer un jeton sécurisé (aussi appelé jeton d’identification) à partir de l’identifiant de l’utilisateur. La date d’expiration du jeton est de 7 jours. Dans la requête HTTP envoyée pour consommer le service REST, le jeton est envoyé dans l’en-tête Authorization avec le mécanisme d’authentification Bearer.

Dans la classe Startup de l’application ASP .NET Core MVC, ajouter les services et les middlewares définis ci-dessous :

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Variables locales;
        IConfigurationSection section;
        byte[] cle;
        Parametres listeParametres;

        // Obtention des paramètres de l'application.
        section = Configuration.GetSection(nameof(Parametres));
        services.Configure<Parametres>(section);
        listeParametres = section.Get<Parametres>();
        cle = Encoding.ASCII.GetBytes(listeParametres.Cle);

        // Configuration de l'authentification JWT.
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.RequireHttpsMetadata = false;
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(cle),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });

        // Activation des services.
        services.AddCors();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddScoped<IUtilisateurService, UtilisateurService>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // Gestion des exceptions.
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Configuration CORS.
        app.UseCors(x => x
            .AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());

        // Activation de l'authentification.
        app.UseAuthentication();

        // Accès aux ressources statiques.
        app.UseStaticFiles();

        // Règles de routage.
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Le service implémenté ci-dessous est le service d’authentification. Il propose une action permettant d’authentifier un utilisateur à partir de son nom et son mot de passe. Cette action retourne une erreur HTTP 401 si ces informations ne sont pas correctes. Un code HTTP 200 avec un jeton est envoyé si l’authentification est réalisée avec succès.

[Route("api/[controller]")]
[ApiController]
public class JwtController : ControllerBase
{
    private IUtilisateurService UtilisateurService { get; }

    public JwtController(IUtilisateurService aUtilisateurService)
    {
        this.UtilisateurService = aUtilisateurService;
    }

    [AllowAnonymous]
    [HttpPost()]
    public IActionResult Authentifier([FromBody]Utilisateur aUtilisateur)
    {
        // Variables locles.
        IActionResult result;

        // Obtention de l'utilisateur à partir de son nom d'utilisateur et de son mot de de passe.
        Utilisateur user = this.UtilisateurService.Authentifier(aUtilisateur.NomUtilisateur, aUtilisateur.MotDePasse);

        // Utilisteur trouvé ?
        result = user == null ?
            (IActionResult)Unauthorized(new { Message = "Nom d'utilisateur ou mot de passe incorrect." }) :
            (IActionResult)Ok(user);
           
        // Retour.
        return result;
    }        
}

Le service REST implémenté ci-dessous permet d’obtenir des données et permettra de les gérer. Pour y accéder, il est nécessaire d’être authentifié ; l’entête de la requête HTTP envoyée doit contenir le jeton d’authentification.

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{

    // GET: api/ThemeFormation
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new List<string>() { "A", "B", "C" };
    }
}

Voici le formulaire de l’application permettant de s’authentifier et consommer le service REST. Le code JavaScript permettant de consommer les services repose sur le Framework JQuery :

<h2>Authentification</h2>
<form>
    Nom d'utilisateur : <input type="text" value="" name="NomUtilisateur" id="NomUtilisateur" />
    <br />
    Mot de passe : <input type="text" value="" name="MotDePasse" id="MotDePasse" />
    <br />
    <br />
    <input type="button" value="Valider" name="CmdValider" id="CmdValider" />
    <br />
    <br />
    <input type="button" value="Accéder aux données" name="CmdAccederDonnees" id="CmdAccederDonnees" />
</form>

@section Scripts
    {
    <script type="text/javascript">
        var token = '';
        $("#CmdValider").click(function (e) {
            var aUtilisateur = {};
            aUtilisateur.NomUtilisateur = $("#NomUtilisateur").val();
            aUtilisateur.MotDePasse = $("#MotDePasse").val();

            $.ajax({
                data: JSON.stringify(aUtilisateur),
                url: "/api/Jwt",
                type: "POST",
                contentType: "application/json;charset=utf-8",
                dataType: "json",
                success: function (response) {
                    token = response.jeton;
                    alert('Authentification réussie.');
                },
                error: function (x, e) {
                    alert(x.status + " - " + x.responseText);
                }
            });
        });

        $("#CmdAccederDonnees").click(function (e) {
            $.get({
                url: "/api/Data",
                headers: {
                    "Authorization": "Bearer " + token,
                    "Accept": "application/json"
                },
                type: "GET",
                success: function (response) { alert('Accès autorisé.'); },
                error: function (x, e) {
                    alert(x.status + " - " + x.statusText);
                }
            });
        });
    </script>
}

Si l’utilisateur consomme le service REST sans être identifié ou tente de s’authentifier avec des informations d’identification incorrectes, alors un code HTTP 401 (Unauthorized) est renvoyé.
Si l’utilisateur s’authentifie avec succès (nom d’utilisateur : algo ; mot de passe : pass), alors un code HTTP 200 est retourné avec un jeton. Puis l’utilisateur peut consommer le service REST et recevoir des flux de données JSON.

About: James RAVAILLE

Travaillant avec la plateforme Microsoft .NET depuis 2002, j’alterne les missions de formation et d’ingénierie avec cette plateforme. J’écris ce blog pour transmettre mes connaissances à tout développeur, qu’il soit débutant ou expérimenté.