--- title: I-CH 183 - 1. Authentification tags: EPSIC slideOptions: theme: moonl spotlight: enabled: false --- <style> .reveal { font-size: 27px; } mark { background-color: rgba(50, 168, 82, .5) !important; } </style> # I-CH 183 Implémenter la sécurité d’une application 1&#46; Authentification © Selmir Hajruli & EPSIC 2022 --- ## Introduction - .NET 6 - ASP.NET Core Web API - ASP.NET Core Identity - EF Core - JWT --- ## Projet de départ - `git clone https://github.com/Selmirrrrr/Epsic.Authx.git` - Application de gestion de tests COVID - Tout le monde ==ressource publique== - Peut voir les stats générales des tests (taux tests négatifs, taux tests positifs, etc) - Un utilisateur connecté ==authentification== - Peut voir les résultats de ses tests à lui - Les médecins ==authorization== - Peuvent insérer des résultats de tests - Voir les résultats pour tous les utilisateurs --- ## JWT Step by Step - `dotnet add package` - `Microsoft.AspNetCore.Authentication.JwtBearer` - `Microsoft.AspNetCore.Identity.EntityFrameworkCore` - Ajouter notre clé secréte au fichier `appsettings.json` ``` "JwtConfig": { "Secret" : "abcdefghij" }, ``` :::info Cette clé secrète va nous servir à signer tokens JWT et ainsi assurer leur intégrité. Cette clé dois absolument rester confidentielle et je JAMAIS transiter vers le client. De plu. la clé doit être composée d'au minimum 32 caractères. ::: - Pour finir, l'on crée une classe `JwtConfig` pour stocker cette config et l'utiliser dans notre back-end : ```csharp public class JwtConfig { public string Secret { get; set; } } ``` --- ## JWT Config - Dans notre classe `Startup.cs` dans la méthode `ConfigureServices()` ajouter la ligne suivante : ```csharp services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig")); ``` :::info Ajouter cette ligne va injecter dans le middleware notre configuration JWT. ::: * Dans la même méthode ajouter : ```csharp services.AddIdentity<IdentityUser, IdentityRole>(options => { }) .AddEntityFrameworkStores<CovidDbContext>(); ``` :::info Ajouter cette ligne va configurer l'utilisation de ASP.NET Core Identity pour la gestion des utilisateurs. ::: --- ## JWT Services - Toujours dans la classe `Startup.cs` dans la méthode `ConfigureServices()` ajouter : ```csharp services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(jwt => { var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]); jwt.SaveToken = true; jwt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, RequireExpirationTime = true, ValidateLifetime = true }; }); ``` :::info Ceci va donc configurer l'authentification JWT pour notre service ainsi que paramétrer la validité ou non des tokens reçus. ::: Note: Ici toute la configuration du JWT Bearer est laissée assez ouverte dans le cadre de l'exercice mais sachez qu'il y a des dizaines de paramètres configurables pour accroitre la sécurité du token, dont les principaux sont : * ValidateIssuerSigningKey * Valide que la clé qui a signé le token est bien la clé que nous avons définie. * IssuerSigningKey = new SymmetricSecurityKey(key) * Défini le clé de signature du token * ValidateIssuer = false * Permet de définir si l'on souhaite valider l'application qui a émis le token, à utiliser en complément avec `ValidIssuer`. * ValidateAudience = false * La même chose `ValidAudience` que mais pour l'audience, à utiliser en complément avec `ValidIssuer`. * RequireExpirationTime = true * Défini si l'on requiert que le token contienne un claim content sa date d'expiration * ValidateLifetime = true * Défini si l'on va valider la date de valider du token, par exemple, si c'est `false` que le token est bon mais simplement expiré l'utilisateur sera quand même autorisé à accéder aux ressources. Mauvaise pratique que de ne pas valider le cycle de vie, surtout en cas de vol de token. Pour la liste de tous les paramètres dispos voir [ici](https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters?view=azure-dotnet). --- ## Authentication Middleware - Pour terminer notre partie setup, dans la classe `Startup.cs` dans la méthode `Configure()` avant : ```csharp app.UseAuthorization(); ``` - Ajouter : ```csharp app.UseAuthentication(); ``` --- ## Identity database - Microsoft ASP.NET Core Identity va implémenter pour nous la gestion des users, roles, tokens, etc. - Pour l'activer il faut, dans notre classe `CovidDbContext` hériter de `IdentityDbContext` ```csharp public class CovidDbContext : IdentityDbContext ``` - Puis faire la migration de la base données pour ajouter les tables nécessaires : ``` dotnet ef migrations add "Adding authentication" dotnet ef database update ``` - Ce qui va nous créer, entre autres, les tables suivantes : - AspNetRoles - AspNetUsers - AspNetUserClaims - AspNetUserRoles - AspNetUserTokens --- ## AuthController - Créer un contrôleur `AuthController` : ```csharp public class AuthManagementController : ControllerBase { private readonly UserManager<IdentityUser> _userManager; private readonly JwtConfig _jwtConfig; public AuthManagementController(UserManager<IdentityUser> userManager, IOptionsMonitor<JwtConfig> optionsMonitor) { _userManager = userManager; _jwtConfig = optionsMonitor.CurrentValue; } } ``` :::info Ici l'on récupère simplement dans le constructeur une instance de `UserManager<IdentityUser>` qui va nous permettre de créer des utilisateurs et de les authentifier. On récupère également notre config JWT configurée précédemment qui nous permettra de signer les tokens avec notre clé personnelle. ::: --- ## Création de compte - Dans ce contrôleur créer une méthode `Register` : ```csharp [HttpPost] [Route("auth/Register")] public async Task<IActionResult> Register([FromBody] RegistrationRequest user) { var newUser = new IdentityUser {Email = user.Email, UserName = user.Email}; var isCreated = await _userManager.CreateAsync(newUser, user.Password); if (isCreated.Succeeded) { var jwtToken = GenerateJwtToken(newUser); return Ok(new RegistrationResponse { Result = true, Token = jwtToken }); } return BadRequest(new RegistrationResponse { Result = false, Message = string.Join(Environment.NewLine, isCreated.Errors.Select(x => x.Description).ToList()) }); } ``` :::info On crée l'utilisateur via le `_userManager`, puis on génère le token JWT dans `GenerateJwtToken(newUser);` et on renvoie le résultat. ::: Note: C'est ASP.NET Core Identity, via le `_userManager` qui fait tout le boulot pour nous : - check du mot de passe (respecte les règles de complexité définies) - hash du mot de passe - stockage en base de données - etc Tout ce qui nous reste à faire c'est créer le token via `GenerateJwtToken(newUser);` --- ## Création de token - Dans le même contrôleur implémenter la méthode `GenerateToken` permettant de créer le token JWT avant de rennvoyer à l'appelant : ```csharp private string GenerateJwtToken(IdentityUser user) { var jwtTokenHandler = new JwtSecurityTokenHandler(); // Nous obtenons notre secret à partir des paramètres de l'application. var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret); // Nous devons utiliser les claims qui sont des propriétés de notre token et qui donnent des informations sur le token. // qui appartiennent à l'utilisateur spécifique à qui il appartient // donc il peut contenir son id, son nom, son email. L'avantage est que ces informations // sont générées par notre serveur qui est valide et de confiance. var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim("Id", user.Id), new Claim(JwtRegisteredClaimNames.Sub, user.Email), new Claim(JwtRegisteredClaimNames.Email, user.Email), }), Expires = DateTime.UtcNow.AddHours(6), // ici, nous ajoutons l'information sur l'algorithme de cryptage qui sera utilisé pour décrypter notre token. SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature) }; var token = jwtTokenHandler.CreateToken(tokenDescriptor); return jwtTokenHandler.WriteToken(token); } ``` Note: Cette méthode va nous générer notre token JWT avec tous les claims associés voulus. * Id : contient l'id de l'utilisateur * Sub : contient le sujet du token (username) * Email : contient le mail de l'utilisateur du token Pour finir, on définit la durée de vie du token à 6 heures. Une liste complète des claims "standards" est disponible ici. Sachez également que l'on peut mettre en place ce que l'on veut dans des claims persos mais il faut veiller à respecter certaines règles : - Jamais d'infos confidentielles dans un token (mot de passe par exemple) - Garder le token le plus petit possible (il est envoyé à chaque requête) - Avoir une durée de vie la plus courte possible et utiliser un refresh token pour rafraichir le token JWT de façon transparente pour l'utilisateur. --- ## RegistrationRequest - Dans le dossier `Models` créer une classe `RegistrationRequest` : ```csharp public class RegistrationRequest { [Required] public string Name { get; set; } [Required] public string Email { get; set; } [Required] public string Password { get; set; } } ``` --- ## RegistrationResponse - Dans le dossier `Models` créer une classe `RegistrationResponse` : ```csharp public class RegistrationResponse { public string Token { get; set; } public bool Result { get; set; } public string Message { get; set; } } ``` --- ## Login - Pour finir, dans ce contrôleur implémenter une méthode `Login` : ```csharp [HttpPost] [Route("auth/Login")] public async Task<IActionResult> Login([FromBody] LoginRequest user) { // Vérifier si l'utilisateur avec le même email existe var existingUser = await _userManager.FindByEmailAsync(user.Email); if (existingUser != null) { // Maintenant, nous devons vérifier si l'utilisateur a entré le bon mot de passe. var isCorrect = await _userManager.CheckPasswordAsync(existingUser, user.Password); if (isCorrect) { var jwtToken = GenerateJwtToken(existingUser); return Ok(new RegistrationResponse { Result = true, Token = jwtToken }); } } // Nous ne voulons pas donner trop d'informations sur la raison de l'échec de la demande pour des raisons de sécurité. return BadRequest(new RegistrationResponse { Result = false, Message = "Invalid authentication request" }); } ``` :::info Ici c'est le `_userManager` de ASP.NET Core Identity qui fait une nouvelle fois tout le travail pour nous. ::: --- ## LoginRequest - Dans le dossier `Models` créer une classe `LoginRequest` : ```csharp public class LoginRequest { [Required] public string Email { get; set; } [Required] public string Password { get; set; } } ``` --- ## LoginResponse - Dans le dossier `Models` créer une classe `LoginResponse` : ```csharp public class LoginResponse { public string Token { get; set; } public bool Result { get; set; } public string Message { get; set; } } ``` --- ## [Authorize] - Pour terminer, pour protéger un endpoint il suffit maintenant d'ajouter la balise `[Authorize]`, par exemple, dans le endpoint `Create` dans `TestsCovidController` : ```csharp [HttpPost("testsCovid")] [Authorize] public async Task<IActionResult> Create([Bind("DateTest,Resultat,TypeDeTest")] TestCovid testCovid) ``` - Après avoir ajouté cette balise nous avons maintenant un `401 Error: Unauthorized` dans Swagger quand on essaye d'ajouter un test, et c'est logique. --- ## Exercices - Dans Swagger créer un utilisateur, se connecter, récupérer le token et trouver une moyen de configurer Swagger pour pouvoir faire nos appels avec le token afin de pouvoir de nouveau créer des tests COVID. - Configurer Microsoft ASP.NET Core Identity pour accepter seulement les mot de passes avec les critères suivants : - Minimum 12 caractères - Au moins 1 minuscule - Au moins 1 majuscule - Minimum 6 caractères différents (AAAAbbbb1234 = ok, AAAAbbbb1111 = ko) - Ajouter dans notre token JWT un claim perso `School` dont la valeur est `Epsic`.