Authentification

L'authentification est une étape importante dans le développement d'une application. Elle permet de vérifier l'identité de l'utilisateur et de lui donner accès à certaines ressources. Dans ce chapitre, nous allons voir comment mettre en place un système d'authentification dans une application ASP.NET.

Nous utiliserons JWT (JSON Web Token) pour sécuriser nos API et nos pages web. JWT est un standard ouvert qui permet de créer des jetons d'accès sécurisés et auto-suffisants. Il est basé sur JSON et est facile à utiliser dans différents langages de programmation.

JWT

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.*

Générer un JWT

Pour générer une JWT nous commençons par générer des Claims. Les Claims sont des informations sur l'utilisateur qui sont stockées dans le JWT. Par exemple, nous pouvons stocker l'identifiant de l'utilisateur, son nom, son email, etc.

using System.Security.Claims;

var claims = new[]
{
    new Claim("Id", "8"),
    new Claim(ClaimTypes.Name, "Roger"),
    new Claim(ClaimTypes.Role, "Admin"),
};

Un claim est composé d'un type et d'une valeur. Le type et la valeur sont tout les deux des chaînes de caractères. Il existe plusieurs types de claims prédéfinis dans .NET, utilisez l'enum ClaimTypes pour les utiliser.

Une fois les claim générés, nous allons créer une clé secrète pour signer le JWT. La clé secrète est utilisée pour vérifier l'intégrité du JWT. Elle doit être gardée secrète et ne doit pas être partagée.

using Microsoft.IdentityModel.Tokens;

SymmetricSecurityKey key = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes("TheSecretKeyThatShouldBeStoredInTheConfiguration")
);
SigningCredentials credentials = new SigningCredentials(
    key, 
    SecurityAlgorithms.HmacSha256
);

Enfin, nous allons créer un token avec les claims et la clé secrète.

using System.IdentityModel.Tokens.Jwt;

JwtSecurityToken token = new JwtSecurityToken(
    issuer: "localhost:5000", // Qui émet le token ici c'est notre API
    audience: "localhost:5000", // Qui peut utiliser le token ici c'est notre API
    claims: claims, // Les informations sur l'utilisateur
    expires: DateTime.Now.AddMinutes(3000), // Date d'expiration du token
    signingCredentials: credentials // La clé secrète
);

string tokenString = new JwtSecurityTokenHandler().WriteToken(token);

Exemple de JWT

Voici un exemple de JWT.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic3RyaW5nIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE3Mjk1OTc0NDUsImlzcyI6ImxvY2FsaG9zdDo1MDAwIiwiYXVkIjoibG9jYWxob3N0OjUwMDAifQ.S17n1mLz34r6Aipb_cbrMebDm4AESdqdF1Ge-XEckkI

Je vous invite a aller sur jwt.io pour décoder ce token et voir son contenu.

Valider un JWT

Nous savons désormais générer un JWT, mais comment s'en servir.

Il faut indiquer à notre API que nous voulons utiliser JWT pour sécuriser nos routes. Nous allons donc ajouter un middleware qui va vérifier la validité du JWT.

program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ClockSkew = TimeSpan.FromMinutes(10), // Temps de tolérance pour la date d'expiration
            ValidateLifetime = true, // Vérifie la date d'expiration
            ValidateIssuerSigningKey = true, // Vérifie la signature
            ValidAudience = "localhost:5000", // Qui peut utiliser le token ici c'est notre API
            ValidIssuer = "localhost:5000", // Qui émet le token ici c'est notre API
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("TheSecretKeyThatShouldBeStoredInTheConfiguration")
            ),
            RoleClaimType = ClaimTypes.Role // Dans quel claim est stocké le role
        };
    });

Via ce middleware, notre API va vérifier la validité du JWT à chaque requête. Si le JWT est valide, l'utilisateur pourra accéder à la ressource demandée. Sinon, il recevra une erreur 401.

Authentification

Activation de l'authentification

Maintenant que nous savons générer et valider un JWT, nous allons voir comment l'utiliser pour sécuriser nos routes.

Il faut d'abord activer l'authentification dans notre API.

program.cs

builder.Services.AddAuthorization();

...

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

Vérification de l'authentification

Pour vérifier l'authentification d'un utilisateur, nous allons ajouter un attribut [Authorize] sur les contrôleurs ou les actions que nous voulons sécuriser.

Voici un exemple d'utilisation de l'attribut [Authorize].


[Authorize] // Indique que l'on vérifie l'authentification de l'utilisateur dans ce contrôleur
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
    [HttpGet]
    [AllowAnonymous] // Permet d'accéder à cette route sans être authentifié
    public IActionResult Anyone()
    {
        return Ok("Hello World");
    }

    [HttpGet]
    [Authorize(Roles = "Admin")] // Permet d'accéder à cette route si l'utilisateur a le role Admin
    public IActionResult Admin()
    {
        return Ok("Hello Admin");
    }

    [HttpGet]
    [Authorize(Roles = "Admin, User")] // Permet d'accéder à cette route si l'utilisateur a le role Admin ou User
    public IActionResult UserOrAdmin()
    {
        return Ok("Hello User or Admin");
    }

    [HttpGet]
    [Authorize] // Permet d'accéder à cette route si l'utilisateur est authentifié peux importe son role
    public IActionResult Authentified()
    {
        return Ok("Hello Authentified");
    }
}

Récupération de l'utilisateur

Pour récupérer l'utilisateur qui a fait la requête, nous allons utiliser la propriété User de l'objet HttpContext.

Si l'on reprend les claims suivant :

var claims = new[]
{
    new Claim("Id", "8"),
    new Claim(ClaimTypes.Name, "Roger"),
    new Claim(ClaimTypes.Role, "Admin"),
};

Voici comment récupérer les informations de cet utilisateur quand il fait une requête.

[HttpGet]
[Authorize] // La route est sécurisée
public IActionResult Get()
{
    int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value); // 8
    string userName = User.FindFirst(ClaimTypes.Name).Value; // Roger
    string userRole = User.FindFirst(ClaimTypes.Role).Value; // Admin

    return Ok(new
    {
        Id = userId,
        Name = userName,
        Role = userRole
    });
}

Ajouter notre JWT dans Swagger

Pour tester nos routes sécurisées, nous allons ajouter notre JWT dans Swagger.

Le code suivant permet d'ajouter un bouton Authorize tout en haut du Swagger.

Program.cs

builder.Services.AddSwaggerGen(option => {
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 1safsfsdfdfd\"",
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                new string[] {}
        }
    }); 
});

Pour éviter de devoir ajouter le JWT à chaque requête, nous allons demander a Swagger de le retenir. Pour cela, nous allons ajouter une option persistAuthorization à Swagger en modifiant l'appel à UseSwaggerUI.

Program.cs

app.UseSwaggerUI(c =>
{
    c.ConfigObject.AdditionalItems.Add("persistAuthorization","true");
});

Quand vous rentrer votre JWT dans Swagger, n'oubliez pas de mettre `Bearer ` devant ex `Bearer MonJwtSuperLong`