Home » Actualité » Mise en place d’une API en .Net 5

Mise en place d’une API en .Net 5

Le 2 février 2022
par Romain D.
Attineos Applications

Introduction

Le .NET 5 ?

Le .NET 5 monte rapidement ces derniers temps. Ceci n’est pas un hasard ! En effet, cette nouvelle mouture de Microsoft a pour objectif de « simplement » remplacer le .NET Framework historique. Contrairement à son aîné, cette technologie est OpenSource et a pour vocation de se rendre plus accessible à tous les systèmes d’exploitation. Un tournant dans la sphère Microsoft, qui a débuté avec le .NET CORE en 2016.

Mais aujourd’hui, nous sommes en 2021 ! Que diriez-vous d’une petite veille technologique sur la dernière version de cet écosystème libre ? Et bien, c’est parti !

Au programme

Pour faire un tour d’horizon de cette technologie, je vais vous proposer une approche plutôt complète. Au-delà de la création pure et simple d’une API, nous allons mettre en place tout ce qui gravite autour de ce type de projet WEB. Voici la feuille de route que je vous propose :

  • Création d’une API en .NET 5
  • Mise en place d’une architecture simple mais posant les bases d’un découpage fonctionnel
  • Manipulation d’une base de données locale
  • Conception de la BDD par l’approche Entity Code First
  • Sécurisation de l’API par Token JWT et rôles utilisateurs

Pré-requis

installation Visual Studio

Lorsque vous sélectionnez « Développement .NET Desktop », n’hésitez pas à vérifier si l’option « SQL Server Express LocalDB » est bien cochée dans la partie droite avant de lancer l’installation.
Installer l’IDE en anglais (Onglet « Modules linguistiques »), pour s’y retrouver parfaitement dans les captures qui vont suivre 😉

Création du projet API

Dans Visual Studio 2019, nous allons faire ce grand classique :

création projet

Ensuite, nous allons sélectionner « ASP.NET Core Web Application » :

choix ASP.NET Core Web Application

Nommons maintenant le projet WEB, la solution et l’emplacement de tout cela :

nom du projet

C’est maintenant que tout commence !

création projet dotnet

Faisons attention à bien choisir :

  • la bonne technologie : .NET CORE
  • la bonne version : ASP.NET CORE 5.0
  • le template ASP.NET Core Web API
  • Et laissons les options cochées par défaut.

    🎉 Félicitations 🎉 Votre API est créée :

api créée

Le Template vous génère un contrôleur « WeatherForecastController » avec une méthode Get d’exemple déjà codée. Vous pouvez y jeter un œil.

Cerise sur le gâteau, notre API est déjà fonctionnelle ! Pour le vérifier, lancez directement le projet après avoir pris soin de sélectionner votre API dans la partie débogage (IIS Express est sélectionné par défaut) :

lancer projet IIS

Il suffit de cliquer sur le bouton TineosProject.API.

Après avoir lancé votre API, deuxième surprise !

Swagger

Swagger (générateur de documentation + testeur d’API) est déjà préinstallé !

Vous pouvez tester votre méthode Get du contrôleur WeatherForecast :

test swagger swagger result

Pour plus d’informations sur Swagger, rendez-vous ici : https://docs.microsoft.com/fr-fr/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-5.0

Base de données locale

Une API qui répond avec des données fictives c’est bien, mais avoir une vraie base de données derrière c’est encore mieux ! Commençons par créer physiquement notre base !

Vérifier l’instance

Avec l’invite de commande, vérifions l’existence de l’instance LocalDB avec la commande :

sqllocaldb info

commande sqllocaldb info

Vérifions qu’elle est démarrée avec :

sqllocaldb info MSSQLLocalDB

Pour aller plus loin :

  • Si l’instance n’est pas démarrée : sqllocaldb start MSSQLLocalDB
  • Si vous souhaitez créer votre instance : sqllocaldb create TineosLocalDB -s (« -s » pour la démarrer directement)
  • Si sqllocaldb n’est pas reconnu en tant que commande, il manque une option à l’installation de Visual Studio (voir plus haut dans la partie prérequis)

Création de la BDD

Ouvrir SQL Server Management Studio (SSMS) et se connecter à la BDD ainsi :

connexion SSMS

Le préfixe « (localdb) » est obligatoire et invariable pour les bases locales comme la nôtre. Ce qui suit correspond au nom de l’instance, dans notre cas « MSSQLLocalDB ».

Une fois connecté, la création se passe dans le panneau de droite :

création base nom de la base

Il n’y a maintenant plus rien à faire du côté de SSMS.

Modélisation de la BDD

Maintenant que notre base est créée physiquement, modélisons-la sous Visual Studio avec l’aide d’Entity Framework. Afin de respecter un découpage fonctionnel, nous allons créer un projet dédié à notre base :

création projet database

Celui-ci sera du type « Class Library (.NET Core) » :

type class library choix dotnet core validation projet database

Class1.cs est à supprimer, sauf s’il est important pour vous de garder le 1er vestige de votre 1ère API .NET 5 pour des raisons sentimentales 😝.

Remarque : Bien vérifier que le nouveau projet créé est en .NET 5. Au moment d’écrire ces lignes, la version par défaut est la CORE 3.1 (clic droit / Propriétés sur le projet Database).

Référençons Entity Framework dans nos projets. Pour cela nous allons utiliser le gestionnaire des packages NuGet :

nuget packages

NuGet est un outil d’installation de dépendances proposé par Visual Studio. Il nous évitera notamment de charger les DLL manuellement et gère aussi le versioning de celles-ci.
Pour plus d’informations sur le sujet, rendez-vous ici : https://docs.microsoft.com/fr-fr/nuget/consume-packages/install-use-packages-visual-studio

Installons les packages suivants dans le projet Database uniquement :

  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Installons le package suivant dans les deux projets :

  • Microsoft.EntityFrameworkCore.Design (le projet API, étant le point d’entrée de notre application, a besoin de cette référence pour générer notre base)

Créons notre tout premier Model Entity en ajoutant une nouvelle classe Tineos.cs dans le projet Database/EntityModels :

création Tineos.cs

Cette classe servira à créer la table « Tineos ».
public class Tineos
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Mail { get; set; }
    public string Password { get; set; }
    public DateTime StartDate { get; set; }
    public string JobFunction { get; set; }
}
Ces propriétés deviendront les champs de la future table « Tineos ».

Remarques :
• Par défaut, Entity Framework reconnaîtra automatiquement les propriétés « Id », « ID » ou « TineosId » comme la clé primaire.
• Si le type de cette propriété est int, une valeur incrémentale sera gérée automatiquement lors des ajouts.
Pour en savoir plus : https://docs.microsoft.com/fr-fr/ef/core/modeling/generated-properties?tabs=data-annotations

Créons maintenant le contexte de notre base de données. Il s’agit du point d’entrée de nos données au niveau du code. Nous devons lister ici tous les modèles Entity que l’on souhaite rendre accessibles par le biais du contexte.

Créons la classe DatabaseContext.cs à la racine du projet Database :

classe DatabaseContext

public class DatabaseContext : DbContext
{
    public DatabaseContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Tineos> Tineos { get; set; }
}
  • Notre classe doit hériter de Microsoft.EntityFrameworkCore.DbContext.
  • Il faut un constructeur permettant de passer des options au DbContext.
  • Et lister les collections des modèles via des propriétés de type DbSet.

Renseignons notre chaîne de connexion dans le projet API, dans le fichier appsettings.Development.json :

"ConnectionStrings": {
  "TineosCnxStr": "Server=(localdb)\\MSSQLLocalDB;Initial Catalog=TineosDatabase;Integrated Security=true;MultipleActiveResultSets=True"
}
Remarque : appsettings.Development.json ne sera pris en compte que pour votre environnement local (ou toute machine où la variable d’environnement ASPNETCORE_ENVIRONMENT a pour valeur « Development »).
Plus d’informations ici : https://docs.microsoft.com/fr-fr/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0

Pour que notre chaîne de connexion soit partagée dans toute l’application, nous allons l’injecter par dépendance. Dans le fichier Startup.cs du projet API, ajoutons l’instruction suivante dans la méthode ConfigureServices() :

services.AddDbContext<DatabaseContext>(
    options => options.UseSqlServer(Configuration.GetConnectionString("TineosCnxStr"))
);
N’oubliez pas de référencer votre projet Database dans votre projet API afin de pouvoir accéder à votre DatabaseContext.

Cette configuration permettra d’injecter le contexte dans les classes voulues, en utilisant automatiquement la chaîne de connexion en paramètre du constructeur réalisé tout à l’heure.

Génération des tables

Les migrations

Pour générer les modèles présents dans notre contexte, il faut créer une migration. Ouvrir le Package Manager Console de Visual Studio :

package manager console

Sélectionner le projet Database par défaut :

sélection projet par défaut

Lancer la commande suivante :

Add-Migration InitialMigration
Un dossier Migrations a été créé dans notre projet Database avec notre fichier de migration (nommé YYYYMMDDHHMMSS_InitialMigration.cs).

Pour mettre à jour la base, il faut lancer cette commande :

Update-Database

On peut observer sur SSMS que la base est à jour :

base mise à jour

Pour chaque changement de modèles Entity (ajout, modification ou suppression), nous devons créer une nouvelle migration pour mettre à jour la base. Les scripts générés seront alors des différentiels par rapport à la dernière migration.

Ajouter des tables à notre BDD existante

Pour approfondir notre exemple, créons un nouveau modèle Project que nous allons lier à Tineos en many-to-many :

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime DeadLine { get; set; }
    public List<ProjectTineos> ProjectTineos { get; set; }
}

Puis, l’entité d’association :

public class ProjectTineos
{
    public int TineosId { get; set; }
    public Tineos Tineos { get; set; }

    public int ProjectId { get; set; }
    public Project Project { get; set; }
}

Précisons qu’un Tineos peut avoir une liste de projets en ajoutant cette propriété :

public List<ProjectTineos> TineosProject { get; set; }

Et complétons les propriétés DatabaseContext en ajoutant la méthode OnModelCreating(), qui va préciser les clés primaires de la table d’association et ajouter quelques données pour les tests :

public class DatabaseContext : DbContext
{
    public DatabaseContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Tineos> Tineos { get; set; }
    public DbSet<Project> Projects { get; set; }
    public DbSet<ProjectTineos> ProjectTineos { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ProjectTineos>()
            .HasKey(s => new { s.ProjectId, s.TineosId });

        var john = new Tineos {
            Id = 1,
            StartDate = DateTime.Now,
            FirstName = "John",
            LastName = "DOE",
            Mail = "john.doe@mail.com",
            Password = PasswordTool.HashPassword("password"),
            JobFunction = "Communication Expert"
        };

        var jeny = new Tineos {
            Id = 2,
            StartDate = DateTime.Now,
            FirstName = "Jeny",
            LastName = "ANDERSON",
            Mail = "jeny.anderson@mail.com",
            Password = PasswordTool.HashPassword("password"),
            JobFunction = "Big Boss"
        };

        var carl = new Tineos {
            Id = 3,
            StartDate = DateTime.Now,
            FirstName = "Carl",
            LastName = "WICK",
            Mail = "carl.wick@mail.com",
            Password = PasswordTool.HashPassword("password"),
            JobFunction = "Lead Developer"
        };

        var google = new Project { Id = 1, StartDate = DateTime.Now, DeadLine = DateTime.Now.AddYears(100), Name = "Buy Google" };
        var piedPipper = new Project { Id = 2, StartDate = DateTime.Now, DeadLine = DateTime.Now.AddYears(2), Name = "Develop new Pied Pipper" };
        var happy = new Project { Id = 3, StartDate = DateTime.Now, DeadLine = DateTime.Now, Name = "Be Happy" };

        var assoGoogle1 = new ProjectTineos { ProjectId = google.Id, TineosId = john.Id };
        var assoGoogle2 = new ProjectTineos { ProjectId = google.Id, TineosId = jeny.Id };
        var assoPp = new ProjectTineos { ProjectId = piedPipper.Id, TineosId = carl.Id };
        var assoHappy1 = new ProjectTineos { ProjectId = happy.Id, TineosId = john.Id };
        var assoHappy2 = new ProjectTineos { ProjectId = happy.Id, TineosId = jeny.Id };
        var assoHappy3 = new ProjectTineos { ProjectId = happy.Id, TineosId = carl.Id };

        modelBuilder.Entity<Tineos>().HasData(john, jeny, carl);
        modelBuilder.Entity<Project>().HasData(google, happy, piedPipper);
        modelBuilder.Entity<ProjectTineos>().HasData(assoHappy1, assoHappy2, assoHappy3, assoPp, assoGoogle1, assoGoogle2);
    }
}

Créons une nouvelle migration :

Add-Migration AddingProjects

Et mettons à jour la BDD :

Update-Database

migration ajout tables


Requêter en base

Afin de continuer notre découpage fonctionnel, créons un nouveau projet Class Library nommé TineosProject.Domain (vérifiez qu’il est bien en .NET 5), et ajoutons-y un dossier Models, Interfaces et Business :

architecture domain

Dans Models, créons une classe TineosModel :

public class TineosModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string JobFunction { get; set; }
    public string Mail { get; set; }
    public DateTime StartDate { get; set; }
}

Dans /Interfaces, créons les interfaces ITineosManager.cs et ITineosBusiness.cs :

public interface ITineosManager
{
    List<TineosModel> GetAllTineos();
}
public interface ITineosBusiness
{
    List<TineosModel> GetAllTineos();
}

Dans /Business, créons TineosBusiness.cs :

public class TineosBusiness : ITineosBusiness
{
    public ITineosManager _tineosManager;

    public TineosBusiness(ITineosManager tineosManager)
    {
        _tineosManager = tineosManager;
    }

    public List<TineosModel> GetAllTineos()
    {
        return _tineosManager.GetAllTineos();
    }
}
L’instance du manager sera passée par injection de dépendance. Nous allons la configurer un peu plus bas.

Dans le projet TineosProject.Database, ajoutons la référence au projet TineosProject.Domain et créons un dossier /Manager contenant la classe TineosManager.cs :

public class TineosManager : ITineosManager
{
    private readonly DatabaseContext _databaseContext;

    public TineosManager(DatabaseContext databaseContext)
    {
        _databaseContext = databaseContext;
    }

    public List<TineosModel> GetAllTineos()
    {
        return _databaseContext.Tineos.Select(t => new TineosModel
        {
            Id = t.Id,
            FirstName = t.FirstName,
            LastName = t.LastName,
            JobFunction = t.JobFunction
        }).ToList();
    }
}
Le contexte sera également passé par injection de dépendance (déjà configurée précédemment).

Direction le Startup.cs du projet TineosProject.API, et ajoutons la ligne suivante dans la méthode ConfigureServices() :

services.TryAddScoped<ITineosManager, TineosManager>();

injection dépendance


Interrogeons notre API

Dans notre projet API, créons un nouveau contrôleur Controller/TineosController.cs :

[ApiController]
[Route("[controller]")]
public class TineosController : ControllerBase
{
    private static ITineosBusiness _tineosBusiness;

    public TineosController(ITineosBusiness tineosBusiness)
    {
        _tineosBusiness = tineosBusiness;
    }

    [HttpGet]
    public ActionResult Get()
    {
        return Ok(_tineosBusiness.GetAllTineos());
    }
}

De la même manière, on configure l’injection de dépendance dans le Startup.cs :

services.TryAddScoped<ITineosBusiness, TineosBusiness>();

injection business

Concernant l’injection de dépendance, il y a plusieurs cycles de vie possibles. Dans notre cas, nous utilisons Scoped afin que chaque requête HTTP utilise la même instance des objets passés en paramètre de constructeur. Il existe deux autres types : Transient (éphémère) et Singleton. Le choix dépend du rôle de ce que l’on souhaite injecter.

Pour plus d’informations, lire cet article :
L’injection de dépendances dans une application ASP.NET Core

On peut maintenant lancer le projet API et tester notre méthode Get() exposée :

test swagger get


Allons plus loin !

Dans le projet TineosProject.Domain, on ajoute une nouvelle signature dans l’interface ITineosBusiness.cs :

public interface ITineosBusiness
{
    List<TineosModel> GetAllTineos();
    TineosModel AddTineos(AddTineosRequest request);
}

Avec un modèle de requête dans /Models/Requests/AddTineosRequest.cs :

public class AddTineosRequest
{
    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    [Required]
    public DateTime StartDate { get; set; }

    [Required]
    public string JobFunction { get; set; }

    [Required]
    public string Mail { get; set; }

    [Required]
    public string Password { get; set; }
}
Il est possible de spécifier dans le modèle d’entrée des champs obligatoires (entre autres) grâce aux DataAnnotations (exemple : [Required]). Ces annotations servent notamment à valider les données d’entrée de notre méthode de contrôleur.

On continue notre chaîne dans ITineosManager.cs :

public interface ITineosManager
{
    List<TineosModel> GetAllTineos();
    TineosModel AddTineos(TineosModel request, string clearPassword);
}

Et on implémente la méthode dans la classe TineosBusiness.cs :

public TineosModel AddTineos(AddTineosRequest request)
{
    var tineosToAdd = new TineosModel
    {
        FirstName = request.FirstName,
        LastName = request.LastName,
        JobFunction = request.JobFunction,
        Mail = request.Mail,
        StartDate = request.StartDate
    };

    return _tineosManager.AddTineos(tineosToAdd, request.Password);
}
Remarques :
• Ces méthodes sont pour l’instant des “passe-plats” vu la simplicité du besoin.
• Les objets passés par le contrôleur vers le business sont les mêmes que ceux envoyés vers le manager, mais en production, il est préférable de découpler ces deux couches.

Dans le projet TineosProject.Database, on implémente la nouvelle méthode dans TineosManager.cs :

public TineosModel AddTineos(TineosModel tineosModel, string clearPassword)
{
    var tineosToAdd = new Tineos
    {
        FirstName = tineosModel.FirstName,
        LastName = tineosModel.LastName,
        StartDate = DateTime.Now,
        JobFunction = tineosModel.JobFunction,
        Mail = tineosModel.Mail,
        Password = PasswordTool.HashPassword(clearPassword)
    };

    _databaseContext.Add(tineosToAdd);
    _databaseContext.SaveChanges();
    tineosModel.Id = tineosToAdd.Id;

    return tineosModel;
}
Remarques :
• Le mapping entre les objets pourrait être automatisé avec une librairie comme AutoMapper.
• Le mot de passe n’est pas stocké en clair : il est haché grâce à PasswordTool.HashPassword().

Voici un exemple de la classe utilitaire de hachage PasswordTool.cs créée dans un dossier /Tools du même projet :

public static class PasswordTool
{
    public static string HashPassword(string password)
    {
        byte[] salt = new byte[128 / 8];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
        }

        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: Encoding.UTF8.GetBytes("saltForPasswordHashing"),
            prf: KeyDerivationPrf.HMACSHA512,
            iterationCount: 10000,
            numBytesRequested: 256 / 8));

        return hashed;
    }
}
💡 Pensez à changer le “salt” et à le variabiliser.

On peut maintenant ajouter notre nouvelle méthode de contrôleur pour insérer un nouveau Tineos :

[HttpPost]
public ActionResult Post(AddTineosRequest request)
{
    if (ModelState.IsValid)
    {
        return Ok(_tineosBusiness.AddTineos(request));
    }
    else
    {
        return BadRequest(ModelState);
    }
}
Un champ obligatoire non renseigné (avec [Required]) invalidera le test ModelState.IsValid. Dans ce cas, une BadRequest sera renvoyée avec le détail de l’erreur.

Testons la nouvelle méthode :

swagger post request swagger post response

On peut constater son ajout en base de données :

donnée ajoutée dans la BDD


Mise en place d’un token d’authentification JWT

Commençons par installer les packages suivants dans le projet TineosProject.API :

  • Microsoft.AspNetCore.Authentication
  • Microsoft.AspNetCore.Authentication.JwtBearer

Ensuite, créons une méthode de génération du Token JWT dans TineosProject.Domain/Tools/TokenTool.cs :

public static class TokenTool
{
    public static string GenerateJwt(TineosModel user, JwtSettings jwtSettings)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
            new Claim(ClaimTypes.Role, user.JobFunction)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var expires = DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings.ExpirationInMinutes));

        var token = new JwtSecurityToken(
            issuer: jwtSettings.Issuer,
            audience: jwtSettings.Issuer,
            claims,
            expires: expires,
            signingCredentials: creds
        );
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Et créons ensuite la classe de configuration suivante : /Models/Config/JwtSettings.cs :

public class JwtSettings
{
    public string Issuer { get; set; }
    public string Secret { get; set; }
    public int ExpirationInMinutes { get; set; }
}
Ainsi que la configuration liée dans le fichier appsettings.Development.json du projet TineosProject.API :
"JwtSettings": {
  "Issuer": "MyTineosAPI",
  "Secret": "JwtS3cr3tK3yWithM@ximumSiz3Is64Byt3s",
  "ExpirationInMinutes": 60
}

Dans le Startup.cs, configurons la partie JWT dans la méthode ConfigureServices() :

// Récupération des paramètres du token JWT dans appsettings.json
var jwtSettings = Configuration.GetSection("JwtSettings").Get<JwtSettings>();

// Configuration de l'authentification et du format de token
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = jwtSettings.Issuer,
        ValidAudience = jwtSettings.Issuer,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)),
        ClockSkew = TimeSpan.Zero
    };
});

Et dans la méthode Configure() :

app.UseAuthentication();
app.UseAuthorization();

Limitons maintenant l’utilisation de notre méthode POST en ajoutant l’attribut [Authorize] à celle-ci :

Authorize sur la méthode Post

Testons maintenant l’accès à nos deux méthodes :

  • Pas de soucis pour la méthode Get() :
    Test GET autorisé
  • La méthode Post() est maintenant inaccessible :
    Post refusé sans token
Nous avons mis l’attribut [Authorize] au-dessus de la méthode Post(), mais il est possible de le placer sur la classe entière. Ainsi, le token sera exigé pour toutes les méthodes du contrôleur sauf mention contraire.

Un attribut [AllowAnonymous] autorisera l’appel sans token, même si la classe est annotée [Authorize].

Pour en savoir plus : Documentation Microsoft sur l’authentification et l’autorisation .

Création du Token

Créons une nouvelle signature et méthode dans ITineosManager.cs et TineosManager.cs pour vérifier la validité des données de connexion :

public TineosModel AuthenticateTineos(string mail, string password)
{
    TineosModel result = null;
    var tineos = _databaseContext.Tineos
        .Where(t => t.Mail == mail)
        .FirstOrDefault();

    if (tineos != null && tineos.Password == PasswordTool.HashPassword(password))
    {
        result = new TineosModel
        {
            Id = tineos.Id,
            FirstName = tineos.FirstName,
            LastName = tineos.LastName,
            JobFunction = tineos.JobFunction,
            Mail = tineos.Mail,
            StartDate = tineos.StartDate
        };
    }

    return result;
}

Ajoutons maintenant une nouvelle interface IAuthBusiness.cs et une classe AuthBusiness.cs :

public interface IAuthBusiness
{
    TokenModel ConnectUser(ConnectUserRequest request);
}
public class AuthBusiness : IAuthBusiness
{
    public ITineosManager _tineosManager;
    private readonly IOptions<JwtSettings> _jwtSettings;

    public AuthBusiness(ITineosManager tineosManager, IOptions<JwtSettings> jwtSettings)
    {
        _tineosManager = tineosManager;
        _jwtSettings = jwtSettings;
    }

    public TokenModel ConnectUser(ConnectUserRequest request)
    {
        TokenModel result = null;
        var existingTineos = _tineosManager.AuthenticateTineos(request.Mail, request.Password);

        if (existingTineos != null)
        {
            result = new TokenModel
            {
                Mail = existingTineos.Mail,
                Token = TokenTool.GenerateJwt(existingTineos, _jwtSettings.Value)
            };
        }

        return result;
    }
}

Et le fichier /Models/Requests/Auth/ConnectUserRequest.cs correspondant :

public class ConnectUserRequest
{
    [Required]
    public string Mail { get; set; }

    [Required]
    public string Password { get; set; }
}
Note : ITineosManager est déjà injecté par dépendance, mais ce n’est pas le cas de JwtSettings. Pour cela, il faut ajouter cette instruction dans le Startup.cs :
services.Configure<JwtSettings>(Configuration.GetSection("JwtSettings"));

Créons maintenant un nouveau contrôleur AuthController.cs :

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private static IAuthBusiness _authBusiness;

    public AuthController(IAuthBusiness authBusiness)
    {
        _authBusiness = authBusiness;
    }

    [HttpPost]
    public ActionResult Login(ConnectUserRequest request)
    {
        if (ModelState.IsValid)
        {
            var token = _authBusiness.ConnectUser(request);
            return token != null ? Ok(token) : BadRequest("Invalid credentials");
        }
        else
        {
            return BadRequest(ModelState);
        }
    }
}

Et comme toujours, on n’oublie pas d’injecter IAuthBusiness dans le Startup.cs :

services.TryAddScoped<IAuthBusiness, AuthBusiness>();

Test de l’authentification

Configurons Swagger dans la méthode ConfigureServices() de Startup.cs pour qu’il puisse injecter un token JWT :

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "TineosProject.API", Version = "v1" });

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Scheme = "Bearer",
        BearerFormat = "JWT",
        Description = "Ajouter le token ainsi : \"Bearer xxxx\" où xxxx est votre token d'authentification",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            new string[] {}
        }
    });
});

Testons maintenant la méthode d’authentification de l’API avec un des comptes présents en base :

test login swagger token jwt généré

Cliquons sur le bouton Authorize en haut à droite de la page Swagger :

swagger authorize

et renseignons notre token fraîchement obtenu (ne pas oublier de mettre Bearer devant) :

ajout du token swagger

Après avoir cliqué sur Authorize, vous pouvez maintenant tester la méthode Tineos/Post que nous avons protégée tout à l’heure :

test post autorisé résultat post autorisé

Affiner par rôle utilisateur

Il est aussi possible de limiter l’accès à une méthode à un rôle en particulier. Si vous avez été attentif tout à l’heure, vous avez remarqué que le rôle est stocké dans le token 🙄. Il est possible de limiter l’accès à un (ou plusieurs) rôle spécifique ainsi :

exemple authorize role

Si nous testons avec le même utilisateur (John Doe) que précédemment la méthode /Tineos/Get, nous obtenons :

test rôle refusé

Si nous récupérons un token avec le tineos Jeny Anderson (dont le rôle est Big Boss), nous obtenons maintenant :

test rôle accepté


Conclusion

Nous arrivons à la fin de cet article. Voici un résumé des notions que vous venez d’aborder :

  • Création d’une Web API en .NET 5 avec une ébauche d’architecture séparant les différentes couches fonctionnelles de notre application (API, Métier et BDD)
  • Création et manipulation d’une base de données locale avec l’approche Entity Code First et LocalDB
  • Manipulation de l’injection de dépendance native .NET 5
  • Sécurisation de l’API par Token JWT et par rôle

J’espère que cet article vous aura permis d’y voir un peu plus clair sur la création d’un projet API .NET 5 from scratch. Le but était de rester simple et de proposer un Getting Started. Les ajustements sont maintenant à votre main, en fonction de vos besoins et contraintes : c’est à vous de coder !

Pour de plus amples informations sur .NET 5, rendez-vous ici : https://docs.microsoft.com/fr-fr/dotnet/fundamentals/

D’autres sujets intéressants