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
.
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.
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()
.
public async Task<IActionResult> Connexion(ConnexionDto connexionDto)
{
var utilisateur = await _userManager.FindByNameAsync(connexionDto.Email);
if (utilisateur == null)
return Unauthorized();
return Ok();
}
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
.
{
"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
}
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 jetonJwtExpirationJours
: 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
.
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.
-
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)
{
} -
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.csprivate 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);
}infoL'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.
-
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"]!));
} -
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.csprivate 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!),
};
}infoIl 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.
-
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.csprivate 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));
}
} -
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.
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.
De plus, vous pouvez inspecter le jeton retourné à l'aide de jwt.io: