Aller au contenu principal

31-2 Authentification auprès de l'API

Suite à la préparation de l'API, on peut poursuivre avec l'authentification: c'est-à-dire recevoir les informations de connexion et retourner un jeton.

Dans un premier temps, le contrôleur recevra, via la fonction Connexion, le DTO de connexion. Ajoutez ce paramètre à la fonction Connexion.

Snowfall.Web.Api/Controllers/AuthController.cs
public async Task<IActionResult> Connexion(ConnexionDto connexionDto)
{
return Ok();
}

Ensuite, il nous faudra SignInManager et UserManager tel que fourni par Identity pour procéder à la tentative de connexion de l'utilisateur.

Injectons donc ces deux dépendances via le constructeur du contrôleur.

Snowfall.Web.Api/Controllers/AuthController.cs
public class AuthController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;

public AuthController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IConfiguration configuration)
{
_signInManager = signInManager;
_userManager = userManager;
}

//...

Valider que l'utilisateur demandé existe

On peut utiliser FindByNameAsync de UserManager pour récupérer l'utilisateur à partir de l'adresse courriel fournie dans le DTO.

Dans le cas où l'utilisateur n'est pas trouvé, on retourne simplement Forbidden().

Snowfall.Web.Api/Controllers/AuthController.cs
public async Task<IActionResult> Connexion(ConnexionDto connexionDto)
{
var utilisateur = await _userManager.FindByNameAsync(connexionDto.Email);

if (utilisateur == null)
return Unauthorized();

return Ok();
}
info

Ajustez si jamais votre utilisateur ne se connecte pas par courriel.

Valider que le mot de passe est valide

Malheureusement, on ne peut pas procéder à la connexion automatique comme on le faisait en MVC et qui nous retournait automatiquement un cookie de session contenant les informations de l'utilisateur.

On doit manuellement vérifier le mot de passe, et si le tout concorde, créer le jeton.

On peut cependant utiliser la fonction CheckPasswordSignInAsync sur UserManager pour valider le mot de passe.

public async Task<IActionResult> Connexion(ConnexionDto connexionDto)
{
var utilisateur = await _userManager.FindByNameAsync(connexionDto.Email);

if (utilisateur == null)
return Unauthorized();

var resultat = await _signInManager.CheckPasswordSignInAsync(
utilisateur,
connexionDto.Password,
false
);

if (!resultat.Succeeded)
return Unauthorized();


return Ok();
}

Créer le jeton

La dernière étape consiste à créer le jeton.

Ajouter les configurations JWT à appsettings.Development.json

Pour générer, signer et valider les jetons, quelques configurations sont nécessaires. On les mettra dans le fichier appsettings.Development.json.

Snowfall.Web.Api/appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"AppDatabaseConnection" : "Server=localhost;Database=snowfall;User Id=postgres;Password=admin;Include Error Detail=true;"
},
"DossierStorage": "../Storage",
"JwtSecurityKey": "7'y&#xmrd,2g<=]k%Q.xLpdYr&J)aD}K",
"JwtIssuer": "http://localhost",
"JwtAudience": "http://localhost",
"JwtExpirationJours": 30
}
info
  • JwtSecurityKey: la clé secrète servant à signer le jeton. J'ai généré une clé aléatoire de 32 caractères, mais vous pouvez mettre ce que vous voulez.
  • JwtIssuer: qui livre et signe le jeton (le serveur)
  • JwtAudience: qui est autorité à utiliser le jeton
  • JwtExpirationJours: délai d'expiration du jeton en jours

Injecter IConfiguration dans AuthController

Pour lire le contenu de appsettings.json, il faut IConfiguration, que nous injections donc dans AuthController.

Snowfall.Web.Api/Controllers/AuthController.cs
public class AuthController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IConfiguration _configuration;

public AuthController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IConfiguration configuration)
{
_signInManager = signInManager;
_userManager = userManager;
_configuration = configuration;
}

Créer une fonction privée CreerToken()

Pour créer un jeton, on encapsulera la logique dans une fonction plutôt que de mettre le tout dans une action du contrôleur.

  1. Créer une fonction qui acceptera un utilisateur (ApplicationUser) et retournera un jeton (string) pour ce dernier.

    Snowfall.Web.Api/Controllers/AuthController.cs
    /// <summary>
    /// Permets de créer un jeton JWT à partir d'un utilisateur
    /// </summary>
    /// <param name="utilisateur">L'utilisateur pour qui créer le jeton</param>
    /// <returns>Jeton JWT au format string</returns>
    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    }
  2. Pour créer un jeton, il faut le signer avec la clé secrète configurée dans appsettings.development.json.

    Snowfall.Web.Api/Controllers/AuthController.cs
    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    // La clé secrète est récupérée de la configuration (appsettings)
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]!));

    // On crée une clé de signature à partir de la clé secrète
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    }
    info

    L'algorithme SHA 256 est utilisé pour signer le jeton à partir de la clé secrète. La signature en tant que telle est effectuée un peu plus loin.

  3. Il faut ensuite calculer la date d'expiration du jeton à partir du nombre de jours de validité configuré dans appsettings.json

    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    // La clé secrète est récupérée de la configuration (appsettings)
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]!));

    // On crée une clé de signature à partir de la clé secrète
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    // La date d'expiration du jeton est configurée en fonction de la durée en jours du jeton
    DateTime expirationDateTime = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpirationJours"]!));
    }
  4. On peut maintenant ajouter des claims (revendications) au jeton, soit des attributs de l'utilisateur qui seront inclus dans le détail du jeton. Cela est très semblable à ce que nous avons fait en MVC pour le cookie d'authentification.

    Snowfall.Web.Api/Controllers/AuthController.cs
    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    // La clé secrète est récupérée de la configuration (appsettings)
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]!));

    // On crée une clé de signature à partir de la clé secrète
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    // La date d'expiration du jeton est configurée en fonction de la durée en jours du jeton
    DateTime expirationDateTime = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpirationJours"]!));

    // Les attributs de l'utilisateur qu'on veut rendre disponible côté client via le jeton
    var claims = new List<Claim>
    {
    new Claim(ClaimTypes.Name, utilisateur.UserName),
    new Claim(ClaimTypes.GivenName, utilisateur.Prenom),
    new Claim(ClaimTypes.Surname, utilisateur.Nom),
    new Claim(JwtRegisteredClaimNames.Email, utilisateur.Email),
    new Claim(JwtRegisteredClaimNames.Sub, utilisateur.Id!),
    };
    }
    info

    Il est possible de modifier les valeurs selon les besoins de l'application, il ne s'agit que d'un point de départ, mais qui devrait couvrir suffisamment de cas de figures.

  5. Une autre information utile pour le client sera le rôle de l'utilisateur ou les rôles s'il en a plusieurs. Il est possible d'utiliser les claims pour cela.

    Snowfall.Web.Api/Controllers/AuthController.cs
    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    // La clé secrète est récupérée de la configuration (appsettings)
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]!));

    // On crée une clé de signature à partir de la clé secrète
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    // La date d'expiration du jeton est configurée en fonction de la durée en jours du jeton
    DateTime expirationDateTime = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpirationJours"]!));

    // Les attributs de l'utilisateur qu'on veut rendre disponible côté client via le jeton
    var claims = new List<Claim>
    {
    new Claim(ClaimTypes.Name, utilisateur.UserName),
    new Claim(ClaimTypes.GivenName, utilisateur.Prenom),
    new Claim(ClaimTypes.Surname, utilisateur.Nom),
    new Claim(JwtRegisteredClaimNames.Email, utilisateur.Email),
    new Claim(JwtRegisteredClaimNames.Sub, utilisateur.Id!),
    };

    var roles = await _userManager.GetRolesAsync(utilisateur);
    foreach (var role in roles)
    {
    claims.Add(new Claim(ClaimTypes.Role, role));
    }
    }
  6. Finalement le jeton est créé, signé et retourné

    private async Task<string> CreerToken(ApplicationUser utilisateur)
    {
    // La clé secrète est récupérée de la configuration (appsettings)
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]!));

    // On crée une clé de signature à partir de la clé secrète
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    // La date d'expiration du jeton est configurée en fonction de la durée en jours du jeton
    DateTime expirationDateTime = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpirationJours"]!));

    // Les attributs de l'utilisateur qu'on veut rendre disponible côté client via le jeton
    var claims = new List<Claim>
    {
    new Claim(ClaimTypes.Name, utilisateur.UserName),
    new Claim(ClaimTypes.GivenName, utilisateur.Prenom),
    new Claim(ClaimTypes.Surname, utilisateur.Nom),
    new Claim(JwtRegisteredClaimNames.Email, utilisateur.Email),
    new Claim(JwtRegisteredClaimNames.Sub, utilisateur.Id!),
    };

    var roles = await _userManager.GetRolesAsync(utilisateur);
    foreach (var role in roles)
    {
    claims.Add(new Claim(ClaimTypes.Role, role));
    }

    var token = new JwtSecurityToken(
    _configuration["JwtIssuer"],
    _configuration["JwtAudience"],
    claims,
    expires: expirationDateTime,
    signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
    }

Retourner le jeton

La fonction CreerToken peut maintenant être utilisée à partir de l'action Connexion afin de créer un jeton pour l'utilisateur nouvellement identifié.

Une fois créé, une nouvelle instance de ResultatConnexionDto est retournée avec le jeton.

Snowfall.Web.Api/Controllers/AuthController.cs
public async Task<IActionResult> Connexion(ConnexionDto connexionDto)
{
var utilisateur = await _userManager.FindByNameAsync(connexionDto.Email);

if (utilisateur == null)
return Unauthorized();

var resultat = await _signInManager.CheckPasswordSignInAsync(
utilisateur,
connexionDto.Password,
false
);

if (!resultat.Succeeded)
return Unauthorized();

string token = await CreerToken(utilisateur);

return Ok(new ResultatConnexionDto() { Token = token });
}

Test

Vous pouvez tester la connexion à l'aide Postman, en utilisant un utilisateur que vous savez être existant.

Imgur

De plus, vous pouvez inspecter le jeton retourné à l'aide de jwt.io:

Imgur