ASP.Net Core – authentification et autorisation – 1 : les bases

La gestion de l’authentification et des autorisations dans ASP.Net Core évolue pas mal par rapport à l’ASP.Net classique. Beaucoup de personnes ont également du mal à appréhender ses concepts en .NET. Cet article vise est une introduction à la gestion de l’authentification et des autorisation en .ASP.Net Core 3.1.

1 – Les concepts
2 – Code : Authentification par Cookie
3 – Code : Ajoutons de l’autorisation
4 – Code : Ajoutons des rôles
5 – Conclusion

1 – Les concepts

Authentification et autorisation

Ces deux notions simples sont étroitement liées mais bien différentes.

  • L’authentification est le fait de savoir qui est la personne qui accède au site Web.
  • L’autorisation de savoir si une personne peut accéder à une ressource protégées (page, api …). Cela sous-entend forcément que la personne a d’abord été authentifiée.

En ASP.Net Core, ces deux principes sont bien présents distinctement.

Les claims

Ce mot peut être littéralement traduit « revendication », mais je traduirais plutôt le concept par « Information ». Pour rester logique, je vais conserver le mot « claim ». Il fait souvent peur aux développeurs car ils ne comprennent pas bien le concept. J’avoue ne pas bien comprendre la notion sous-jacente car il s’agit de simples propriétés liées à l’utilisateur.

En .Net, toute personne étant authentifiée se voir attribuer :
– Un claim principal content une liste d’identité pour l’utilisateur connecté.
– Chaque identité ayant une liste d’information sur l’utilisateur. Plusieurs sont définis par défaut dans la documentation ClaimTypes, mais vous pouvez déclarer tout ce que vous voulez.

Autre dit, lorsqu’un utilisateur est authentifié sur un site Web, il lui ait associé des claims. Par exemple, s’il s’agit d’une authentification Windows par Active Directory, les claims seront des propriétés de l’active directory. Il est possible de les compléter via l’interface « IClaimsTransformation » mais c’est un autre sujet.

Les schemes

Un « scheme » se traduit littéralement par schéma. Cela correspond au schéma d’authentification qui inclue un service d’authentification et sa configuration. Autrement dit, cela définie le moyen par lequel l’utilisateur va s’authentifier sur le site Web.

Bien qu’il soit possible de créer son schéma de toute pièce, ASP.Net Core fournie déjà un ensemble de schémas prédéfinis. Je n’ai pas trouvé de documentation faisant une liste exhaustive des schéma. Le tableau suivant liste ceux que je connais.

CookieAuthenticationDefaultsUtilisation d’un simple cookie
JwtBearerDefaultsUtilisation d’un token JWT (Json Web Token)
OpenIdConnectDefaults OpenId est une surcouche utilisant protocole OAuth2 avec un token JWT qui permet la fédération d’identité entre plusieurs sites différents.
FacebookDefaults
GoogleDefaults
MicrosoftAccountDefaults
TwitterDefaults
Utilisation de service tiers pour authentifier un utilisateur.
IISDefaultsUtilisation du serveur IIS (par Active Directory) avec session gérée dans le processus IIS (In-Proc)
IISServerDefaultsUtilisateur du serveur IIS (par Active Directory) avec session gérée par un processus externe (Out-Proc via SessionStateManager)

Ce concept est donc important puisque c’est bien le « scheme » qui va définir la façon dont les utilisateur vont pouvoir s’authentifier dans ASP.Net Core.

2 – Code : authentification par Cookie

Dans cet exemple, j’ai créé un site Web Api simple sans authentification. Le test se fait avec le logiciel gratuit Postman.

Qu’allons nous faire ?

Le but est d’avoir un contrôleur Web API qui permette à un utilisateur connecté de s’authentifier. Par la suite, nous vérifierons que cette personne est autorisée à accéder à un second contrôleur Web API protégé.

Création de l’API d’authentification

a – Le code du contrôleur

La première étape que nous allons faire est la création d’un contrôleur permettant d’enregistrer une personne. Pour cela, il est nécessaire de lui créer une identité avec des claims. Le tout servant à s’authentifier avec un cookie.

Nous avons un contrôleur « LoginController » avec une méthode « Login » permettant d’authentifier l’utilisateur, et une méthode « Get » récupérant l’utilisateur authentifié.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
namespace clemovernet.web.Controllers
{
    [Route("api/[controller]"), ApiController]
    public class LoginController : ControllerBase {

        [HttpGet, Route("Sign")]
        public IActionResult Sign() {
            // création de 3 claims inclus dans une identité
            var claims = new List<Claim> { 
                new Claim(ClaimTypes.Name, "Mister T"), 
                new Claim(ClaimTypes.Email, "mister.t@mail.com"), 
                new Claim("Test_claim", "Super") 
            };
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); 
            var principal = new ClaimsPrincipal(identity);

            // Authentification de l'utilisateur
            HttpContext.SignInAsync(principal); 

            return Ok();
        }

        [HttpGet, Route("User")]
        public IActionResult Get() {
            var obj = new { 
                IsAuthenticated = User.Identity.IsAuthenticated, 
                UserName = User.Identity.Name,
                Claims = User.Identities.FirstOrDefault().Claims
.Select(x => new { x.Type, x.Value })
            }; 
            return Ok(obj);
        }

        [HttpGet, Route("Logout")]
        public IActionResult Logout() {
            HttpContext.SignOutAsync();
            return Ok();
        }
    }

}

L’idée étant d’avoir une notion simple du code, il n’y a pas de notion de login / mot de passe. Bien sûr, vous pourriez vérifier ces informations avant d’appeler la méthode signature « SignInAsync ». Une fois qu’un utilisateur accèdera à la page, nous pourrons consulter son identifier via « HttpContext.User ».

b – Déclaration et configuration des services

ASP.Net Core fonctionne par un système d’injection de dépendance dont la configuration se fait dans le fichier « Startup.cs ».

Commençons par ajouter notre service d’authentification en ajouter la gestion des cookies. Le paramètre dans « AddAuthentication » sert à indiquer le schéma à utiliser par défaut, notamment lorsqu’il y en a plusieurs. Nous avons mis « AddCookie », nous pourrions gérer plusieurs types en plus (JWT par exemple).

public void ConfigureServices(IServiceCollection services) {
    services.AddControllers();
    services.AddAuthentication(
        CookieAuthenticationDefaults.AuthenticationScheme
     )
    .AddCookie();
}

Ensuite, nous pouvons configurer le pipeline d’utilisation des services déclarés. L’ordre de déclaration est très important.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 
    if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } 

    app.UseRouting();
    app.UseAuthentication();
    app.UseEndpoints(endpoints => endpoints.MapControllers()); 
}

c – Tests

Lançons le projet Web API en debug et ouvrons PostMan.

1er appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
Le résultat est assez clair, l’utilisateur n’a aucune information et n’est pas authentifié !

{
    "isAuthenticated": false,
    "userName": null,
    "claims": {}
}

2nd appel : authentifions l’utilisateur (GET http://localhost:59537/api/Login/Sign)
Le retour se fait sans erreur, et il y a un cookie qui est renvoyé.
Il s’appelle « .AspNetCore.Cookies » et contient une chaîne de caractère chiffrée liée à la machine. Son contenu est une description de l’identité de l’utilisateur.

3ème appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
Cette fois-ci, le résultat est différent du 1er appel, nous avons nos informations !

{
    "isAuthenticated": true,
    "userName": "Mister T",
    "claims": {
        {
            "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
            "value": "Mister T"
        }, {
            "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
            "value": "mister.t@mail.com"
        }, {
            "type": "Test_claim",
            "value": "Super"
        },
    }
}

L’utilisateur est bien authentifié avec sa liste de claims. Nous noterons que le claim « name » est utilisé dans « User.Identity.UserName ».

4ème appel : déconnexion de l’utilisateur (GET http://localhost:59537/api/Login/Logout)
Nous appelons la méthode de déconnexion, le cookie ASP.Net Core disparait.
Si nous rappelons l’API « User », nous revenons au résultat du 1er appel.

Nous avons donc vu comment authentifier simplement un utilisateur via un cookie sur une Web API. Cette technique est simple et ne permet pas, par exemple, de fédération avec d’autres sites / API puisque le cookie est lié à la machine qui l’a généré.

3 – Code : Ajoutons de l’autorisation

Maintenant que nous savons qui se connecter à notre site, nous voudrions pouvoir définir s’il a le droit ou non d’accéder à certains contrôleur ou méthodes. Pour faire cela, il existe l’attribut « Authorize » qui se déclare sur des classes de contrôleur ou sur des méthodes de contrôleur.

Si un contrôleur / méthode a l’attribut « Authorize », alors l’utilisateur doit être authentifié. Dans le cas d’un schéma d’authentification par cookie, si l’utilisateur n’est pas authentifié, il est redirigé vers la page de login.

De plus, pour information, si un contrôleur a l’attribut « Authorize », il existe l’attribut de méthode « AllowAnonymous » qui a l’effet inverse. Il autorise une personne non authentifiée à accéder à la méthode.

Gestion d’une autorisation simple

a – Le code du contrôleur

Nous allons modifier les méthodes « Get » pour ajouter l’attribut « Authorize ».
Pour la méthode « Logout », nous allons tester si l’utilisateur est autentifié.

[HttpGet, Route("User"), Authorize]
public IActionResult Get() { ... }

[HttpGet, Route("Logout")] 
public IActionResult Logout() { 
    if (User.Identity.IsAuthenticated) { 
        HttpContext.SignOutAsync(); 
    } else {
        return Unauthorized(); 
    }
    return Ok(); 
}

b – Déclaration et configuration des services

Comme décrit dans l’introduction, si l’utilisateur n’est pas authentifié, alors il est redirigé vers la page d’authentification. Cette page est configuré dans la définition du service.

De plus, comme nous utilisons l’authentification, il faut ajouter cette notion dans le pipeline d’utilisation des services. Elle doit obligatoirement être déclarée après la notion d’authentification (restons logique ;))

public void ConfigureServices(IServiceCollection services) {
    services.AddControllers(); 
    services.AddAuthentication(
       CookieAuthenticationDefaults.AuthenticationScheme) 
            .AddCookie(opt => { opt.LoginPath = "/api/Login/Sign"; });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 
    if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } 
    app.UseRouting();

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

    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

C’est la propriété « LoginPath » qui permet de définir la page de login. Dans l’exemple, nous renvoyons directement sur la méthode authentifiant l’utilisateur. Vous pouvez consulter la liste des options existantes pour le schéma d’authentification par cookie.

c – Tests

Le test commence avec un utilisateur qui n’est pas authentifié sur le site.

1er appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
Lors de cet appel, l’attribut « Authorize » teste si la propriété « User.isAuthenticated » est vrai. Comme ce n’est pas le cas, la requête HTTP est redirigée vers l’API d’authentification « api/Login/Sign ». En résultat, le cookie d’authentification est créé.

2nd appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
Pour cet appel, nous sommes bien authentifié, alors la réponse contient le détail de l’utilisateur.

3ème appel : déconnexion de l’utilisateur(GET http://localhost:59537/api/Login/Logout)
Le cookie d’authentification disparait.

3ème appel : rappel de la déconnexion de l’utilisateur(GET http://localhost:59537/api/Login/Logout)*
Cette fois-ci, l’utilisateur n’est plus authentifié, alors nous recevons un retour 401.

{
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1",
    "title": "Unauthorized",
    "status": 401,
    "traceId": "|386aedd8-441380e7d2093549."
}

Tout fonctionne correctement, l’API protégée est accédée uniquement lorsque l’utilisateur est authentifié. Nous allons voir un dernier point qui consiste à autoriser l’accès à une API selon le rôle de l’utilisateur.

4 – Code : Ajoutons des rôles

Le rôle d’un utilisateur est défini par un claim particulier. il s’agit donc d’une simple propriété de l’utilisateur qui sera interprété par l’ASP.Net Core.

Gestion d’une rôle administrateur

a – Le code du contrôleur

Comme décrit dans l’introduction, il faut ajouter un nouveau claim « rôle » dans la construction de l’identité de l’utilisateur. Pour cela, c’est très simple, il faut suffit de compléter la méthode « Sign » :

var claims = new List<Claim> {
    new Claim(ClaimTypes.Name, "Mister T"), 
    new Claim(ClaimTypes.Email, "mister.t@mail.com"), 
    new Claim("Test_claim", "Super"), 
    new Claim(ClaimTypes.Role, "Admin"), 
    new Claim(ClaimTypes.Role, "Member") 
};

Dans l’exemple, j’ai ajouté 2 rôles à l’utilisateur : « Admin » et « Member ».

Pour vérifier si un utilisateur a bien un rôle lors de l’appel d’un contrôleur ou d’une méthode, il faut ajouter ce rôle en paramètre de l’attribut « Authorize ». Modifions donc la méthode « Get » dans ce sens.

[HttpGet, Route("User"), Authorize(Roles = "Admin")]
public IActionResult Get() { /* ... */ }

A noter 2 points importants :
– Si plusieurs rôles peuvent accéder à la ressource, il faut les séparer par une virgule « Admin,Member »
– Si un utilisateur doit avoir obligatoirement plusieurs rôles, il faut dupliquer l’attribut « Authorize »

Enfin, ajoutons une nouvelle méthode « AccessDenied » qui sera appelée lorsqu’un utilisateur n’a pas l’autorisation d’accéder à une méthode protégée par un rôle. La route est configurable via la propriété « AccessDeniedPath » du service de gestion des cookie.

[HttpGet, Route("AccessDenied")]
public IActionResult AccessDenied()
{
    return Ok("Accès refusé");
}

Je vous invite à consulter la documentation officielle pour gérer des notions plus complexes autour des rôles comme les « Policies ».

b – Déclaration de configuration des services

Nous avons simplement à compléter la propriété du chemin d’accès refusé utilisée par le service d’authentification par cookie avec la nouvelle méthode « AccessDenied ».

public void ConfigureServices(IServiceCollection services){
    services.AddControllers(); 
    services.AddAuthentication(
                  CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(opt => {
                    opt.LoginPath = "/api/Login/Sign";
                    opt.AccessDeniedPath = "/api/Login/AccessDenied";  
    });
}

b – Tests

Nous pouvons tester que notre utilisateur « Admin » peut accéder à la méthode « Get ».
Nous partons du principe que l’utilisateur n’est pas authentifié.

1er appel : authentifions l’utilisateur (GET http://localhost:59537/api/Login/Sign)
En résultat, le cookie d’authentification ASP.Net Core est bien créé.

2nd appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
L’appel fonctionne bien et nous retrouvons toutes les informations de l’utilisateur.

c – Modifions le rôle de l’API

Pour vérifier que la gestion par rôle fonctionne bien, nous pouvons modifier l’attribut « Authorize » de la méthode « Get » pour changer le rôle.

[HttpGet, Route("User"), Authorize(Roles = "SuperAdmin")]
public IActionResult Get() { /* ... */ }

d – Tests 2

Nous partons du principe que l’utilisateur est déjà authentifié.

2nd appel : récupérons l’utilisateur (GET http://localhost:59537/api/Login/User)
La magie opère et nous renvoie le texte « Accès refusé ».

5 – Conclusion

Ce tutoriel présente vraiment les bases à connaitre sur l’authentification et l’autorisation des utilisateurs avec l’ASP.Net Core. Tous les autres systèmes d’authentification se basent sur les mêmes principes d’identité, de claim et de rôle.

Par exemple, l’attribut « Authorize » sera toujours l’élément vous permettant de gérer l’autorisation d’une personne à accéder à une méthode. Il peut être étendu via un héritage pour ajouter des propriétés personnalisées.

La seule particularité que nous avons vu est que les informations de l’utilisateur sont persistées dans un cookie d’authentification, alors que les autres systèmes utiliseront d’autres moyens (token json, active directory …).

Dans de prochains tutoriels, nous verrons la couche « ASP.Net Core Identity » qui est un middleware servant à gérer la persistance des utilisateurs. Cela évite d’avoir un créer son propre système de gestion des utilisateurs.