Fin du support de IE11 (j’ai l’impression que c’est dans chaque version depuis la 10)
Amélioration d’Angular CLI avec un cache de build qui améliore le temps de compilation
Du point de vue d’un développeur lambda, il s’agit de petites nouveautés qui améliorent la vie 🙂
A noter que comme chaque nouvelle sortie du Framework, une suppression du dossier « node_modules » et du fichier « package.lock.json » a été nécessaire.
Microsoft continue à refaire le Framework .NET en version Open Source / multi-plaforme et plus rapide. Petit problème pour le moment, le SDK existe uniquement pour Visual Studio 2022 (preview), il faudra attendre un peu pour avoir la compatibilité avec VS 2019.
Le développement en HTTPS (certificat SSL) permet d’être iso-production. Cependant, il est souvent compliqué d’avoir un certificat qui fonctionne avec les navigateurs, surtout depuis que Chrome est devenu difficile sur le sujet. Cet article vous montre comment configurer un certificat SSL valide sous Chrome pour travailler en HTTPS avec Angular, le tout en 5 minutes !
1. Générer un certificat de développement
J’ai trouvé 2 solutions possibles : – Soit vous avez créé un projet sous Visual Studio 2019 avec le SSL activé, dans ce cas Visual Studio 2019 se charge de la création du certificat – Soit vous pouvez le générer manuellement via la commande suivante : (pas besoin de se prendre la tête avec openssl !)
Nécessite le SDK .NET 5.0
dotnet dev-certs https --trust
2. Récupérer le certificat de développement .NET
Il nous faut d’avoir récupérer le certificat généré par le .NET. Ma solution est simple, mais fonctionne uniquement avec Windows … il doit y avoir un moyen similaire sous Mac / Linux.
Ouvrez une console de management [Windows+R > mmc]
Menu « Fichier > Ajouter / Supprimer un composants enfichable… »
Sélectionnez « Certificats », cliquez sur « Ajoutez », sélectionnez « Un compte d’ordinateur », cliquez sur « Suivant > Terminer »
Cliquer sur OK pour revenir à la console avec la liste des certificats
Voilà notre certificat « IIS Express Development Certificate » ! Nous noteras qu’il est valable 5 ans, ca laisse du temps 😉
Double cliquez sur le certificat pour l’ouvrir
Allez dans « Details », cliquez sur « Copiez dans un fichier … »
Cliquez sur « Suivant » et sélectionnez « Oui, exporter la clé privée« , puis cliquez sur « Suivant > Suivant »
Cochez « Mot de passe » et saisissez-en un, « localhost » par exemple, puis « Suivant »
Entrez le chemin du fichier à sauvegarder, par exemple « C:\localhost.pfx », puis cliquez sur « Suivant » et enfin « Terminer »
=> L’archive « C:\localhost.pfx » est généré !
3. Extraire le certificat et sa clé
Malheureusement, Angular ne sait pas prendre en entrée un fichier PFX. Il faut extraire le certificat et sa clé manuellement. Pour cette partie, OpenSSL est nécessaire, mais pas besoin d’une autre installation 🙂 – Commencez par récupérer les binaire sur le site de SourceForge. – Dézippez l’archive dans « C:\openssl » : attention, ce point est important [l’exécutable va chercher un fichier dans ce chemin]
Ouvrir une commande Windows, placez vous dans « C:\openssl\bin » et exécutez les commandes suivantes :
Dans le premier article dédié à l’authentification, nous avons vu une version très simple permettant d’identifier un utilisateur via un cookie, ainsi que les principes de base qui vont avec. Pour rappel, l’élément important est le « claim » qui représente une information d’un utilisateur.
Dans ce nouvel article, nous allons aller plus loin pour gérer un ensemble d’utilisateur. En effet, gérer soit même les utilisateurs est assez fastidieux quand le .NET fournit déjà une librairie par défaut qui nous simplifie la vie. Je n’ai trouvé aucun article résumant les choses en partant d’un projet vierge.
La page du site de Microsoft présente déjà la librairie. Il s’agit d’un middleware permettant la gestion des utilisateurs. Il est capable de fournir des pages par défaut pour gérer la création des utilisateurs, leur connexion … avec des possibilités d’envoi de mail de confirmation …
L’avantage est que la gestion des utilisateurs est très simple, l’inconvénient est que la boîte noire contient un tas de fonctionnalités plus ou moins utiles. Cet article va donc présenter les grands points à connaitre pour utiliser ASP.Net Core Identity. Ce passage est obligé pour monter d’un cran et gérer simplement de l’authentification par token json, mais il s’agira d’un prochaine article 😉
ASP.Net Core Identity fournit les services suivants :
Un schéma de base de données
Un service « UserManager » pour la gestion des utilisateurs (création, recherche …)
Un service « RoleManager » pour la gestio des rôles (création, suppression …)
Un service « SignInManager » pour la gestion de l’authentification des utilisateurs
2. Création de la solution Web Api et de la base de données
Le premier point est de créer une nouvelle solution de type « API » en ASP.Net Core 5 sans authentification, nous allons tout faire à la main pour bien comprendre le fonctionnement.
La première étape consiste à ajouter les références de package Nuget suivantes dans notre projet :
Le dernier sert uniquement pour que « migration » fonctionne lors de la création de la base de données.
2.1. Création du contexte EntityFramework
Pour que les informations soient persistées, nous devons initialiser un contexte EntityFramework spécial pour la gestion des utilisateurs et des rôle. Créons le fichier « ApplicationDbContext.cs » avec le contenu suivant.
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace IdentityServer.Models
{
public class ApplicationDbContext : IdentityDbContext<IdentityUser> {
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}
}
}
Le contexte EntityFramework Identity est décrit dans la classe « IdentityDbContext ». Nous devons créer une classe héritant de ce contexte pour l’utilisée dans notre application. De plus, il prend en paramètre un type d’objet qui doit hériter de la classe « IdentityUser ». Dans l’exemple, nous utilisons directement « IdentityUser », mais il est possible de déclarer un objet spécifique à son application et de lui ajouter d’autres propriétés qui seront persistées.
2.2. Configuration du service et de l’application
Tout d’abord, ajoutons une chaîne de connexion dans le fichier « appsettings.json ».
Ensuite, nous pouvons commencer la déclaration des services du fichier « Startup.cs » :
public void ConfigureServices(IServiceCollection services) {
var connectionString = _configuration.GetConnectionString("IdentityServerDatabase");
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddControllers();
}
Assez classiquement, nous commençons par déclarer le contexte EntityFramework lié à Sql Server. En plus, nous déclarons le middleware « Identity » qui prend 2 types génériques :
Le type de classe d’un utilisateur qui doit être le même que celui du contexte, nous mettons donc « IdentityUser »
Le type de classe d’un rôle, il est possible d’en définir un personnalité qui hérite de la classe « IdentityRole », à défaut l’utilisateur d' »IdentityRole » est suffisante.
Vous noterez que nous ne faisons pas appel à la méthode « services.AddAuthentification() » car celle-ci est appelée implicitement par le « AddIdentity ». Je vous invite à aller voir le code de la méthode sur GitHub pour voir tout ce qu’elle fait.
Attention, il existe 3 méthodes similaires :
AddIdentityCore : gère uniquement les utilisateurs, sans cookie, sans rôles
AddIdentity : inclus AddIdentityCore avec la gestion des cookies et des rôles
AddDefaultIdentity :inclus AddIdentityCore et des IHM par défaut pour les inscriptions, login …
Enfin, configurons les services actifs.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
Nous faisons simplement un appel à « UseAuthentification » pour qu’elle soit activée et c’est terminé pour une version simple 🙂
2.3. Création de la base de données
ASP.Net Core Identity utilise « Migration » pour initialiser la base données.
Tout d’abord, si vous n’avez pas les commandes dotnet de migration d’installée (non installées par défaut), il faut installer l’outil « ef » via un terminal.
dotnet tool install --global dotnet-ef
Ensuite, vous pouvez lancer la création de la base de données via un terminal (menu « Affichage » de Visual Studio). Deux points à noter : compilez la solution, et placez-vous bien dans le répertoire du projet concerné (et non au niveau de la solution) .
dotnet ef migrations add InitIdentity
dotnet ef database update
Notre base de données est maintenant initialisée !
3. Création du contrôleur de gestion des utilisateurs
Nous sommes prêts à démarrer notre projet et notre gestion des utilisateurs. Nous créons un nouveau contrôleur API « AuthController ».
3.1. Constructeur
Notre injectons les services de gestion des utilisateurs « UserManager » et d’authentification « SignInManager » dans le contrôleur. Ceux-ci prennent en argument générique le type de la classe représentant un utilisateur.
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace IdentityServer.Controllers {
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase {
private UserManager<IdentityUser> _userManager;
private SignInManager<IdentityUser> _signInManager;
public AuthController(UserManager<IdentityUser> userManager
,SignInManager<IdentityUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
}
}
3.2. Création d’un utilisateur
La première méthode de notre contrôleur sert à créer un utilisateur. Nous allons faire simple en passant un login / mot de passe en paramètre.
[HttpGet, Route("Create")]
public async Task<IActionResult> Create(string login, string password) {
var model = new IdentityUser { UserName = login };
var result = await _userManager.CreateAsync(model, password);
return Ok(result);
}
Notez le passage en argument du mot de passe plutôt que le mettre dans la propriété du modèle pour qu’il soit encrypté.
Vous noterez que si vous mettez un mot de passe trop simple, vous aurez des erreurs. Idem si vous essayez de créer 2 fois le même utilisateur. C’est le comportement intégré de ASP.Net Core Identity qui gère cela. Nous verrons dans le dernier paragraphe comment modifier ces comportements.
3.3. Login / logout
Après avoir créé un utilisateur, il est intéressant de pouvoir être authentifier ou terminer sa session. Nous créons ainsi les 2 méthodes liées à ces foncitonnalités.
[HttpGet, Route("Login")]
public async Task<IActionResult> Login(string login, string password) {
var model = await _userManager.FindByNameAsync(login);
if (model != null) {
var result = await _signInManager.PasswordSignInAsync(model, password, false, false);
return Ok(result);
}
return NotFound();
}
[HttpGet, Route("Logout")]
public async Task<IActionResult> Logout() {
if (_signInManager.IsSignedIn(User)) await _signInManager.SignOutAsync();
return Ok();
}
Le résultat de l’appel de l’URL « https://localhost:44393/api/Auth/GetMe » renvoie les information de l’utilisateur « User ». Pour rappel, « User » est l’objet représentant l’utilisateur connecté en ASP.Net.
4. Ajoutons des authorisations et un rôle d’administrateur
Pour finir, nous allons ajouter la notion d’autorisation qui est essentielle dans toute application. En effet, tout utilisateur ne peut pas faire ce qu’il veut ! Ainsi, nous ajouterons un rôle Administrateur permettant de rechercher un utilisateur par son nom.
4.1. Activation de l’autorisation
C’est assez simple, il suffit de déclarer le service et de l’activer.
Attention a bien mettre le « app.UseAuthorization() » après le « UseRouting », sinon ca ne fonctionnera pas. Il y a une logique dans l’ordre d’activation des services qui fonctionnent en pipe. En effet, ils sont exécutés les uns après les autres pour chaque requête, et pour savoir si un contrôleur ou une méthode nécessite de vérifier l’autorisation, il faut d’abord qu’il soit trouvé via le routage.
4.2. Ajout de la méthode de recherche d’un utilisateur
Nous créons une méthode permettant de rechercher un utilisateur par son login, elle nécessite que l’utilisateur soit autorisé. Sans préciser de rôle, cela revient à dire qu’il est simplement authentifié.
Appelons le login, et rappelons la méthode de recherche. => L’utilisateur est bien retourné comme avec le « GetMe ».
4.3. Ajout d’un rôle administrateur
Toute personne connectée ne devrait pas pouvoir rechercher les utilisateurs, nous allons créer un rôle « Admin » spécifique pour la méthode de recherche.
Nous ajoutons d’abord le service de gestion des rôles « RoleManager » qui prend en argument générique le type de la classe des rôles.
Créons ensuite une méthode pour ajouter le rôle à un utilisateur.
[HttpGet, Route("SetRole")]
public async Task<IActionResult> SetRole(string login)
{
var role = await _roleManager.FindByNameAsync("Admin");
if (role == null) await _roleManager.CreateAsync(new IdentityRole { Name = "Admin" });
var model = await _userManager.FindByNameAsync(login);
if (model != null) await _userManager.AddToRolesAsync(model, new[] { "Admin" });
return Ok();
}
La méthode se comprend d’elle même. Bien sûr, elle n’est pas protégée, il ne faudrait pas la laisser en production 😉 La méthode « https://localhost:44393/api/Auth/SetRole?login=Clemox » permet d’ajouter le rôle « Admin » à l’utilisateur « Clemox ».
Nous pouvons modifier la méthode « Search » pour indiquer qu’il faut que l’utilisateur connecté est le rôle « Admin ».
Il faut refaire un appel à l’URl de login pour mettre à jour le cookie d’authentification. Ensuite, l’appel à la méthode « Search » fonctionne uniquement pour l’utilisateur « Clemox » !
La magie du .Net a à nouveau opéré 😀
5. Pour aller plus loin dans la configuration
La librairie ASP.Net Identitiy fournie plusieurs possibilités de configuration par défaut vis-à-vis de la gestion du mot de passe, par exemple. Vous pouvez configurer cela dans le fichier « Startup.cs », dans la méthode de déclartion des services.
Egalement, vous pouvez configurer le cookie généré via la méthode spéciale « ConfigurationApplicationCookie ». Notamment, il est possible de changer le nom du cookie ou la page de direction.
En regardant plus en détail dans ces méthodes, ainsi que les services « UserManager, « SignInManager » et « RoleManager », vous pourrez gérer facilement vos utilisateurs et leur rôle !
Enfin, la librairie peut aussi gérer des interactions avec des systèmes externes : Google, Facebook et Microsoft.
Cette fin d’année est assez riche en nouveautés puisque nous pouvons noter 2 sorties majeures, et Microsoft met à disposition un nouveau site Web d’entraide autour du.NET.
.NET 5
Fini le Framework .NET et .NET Core, aujourd’hui, nous ne parlons plus que du .NET 5. En réalité, il s’agit de la continuité du .NET Core 3 qui a directement été nommé « .NET 5 » pour bien indiquer qu’il n’y aura plus d’évolution du côté du Framework .NET.
Pour rappel, le .NET 5 est la plateforme unifiée et open-source de Microsoft qui doit répondre à tous les besoins d’applications : Web, client lourd, jeux videos, IA, objets connectés …
A noter également que cette version .NET 5 n’est pas une LTS (long term support). Une nouvelle version majeure du .NET sortira chaque année, et seule 1 sur 2 sera en LTS. La prochaine LTS sera le .NET 6.
Angular 11
La dernière version d’Angular est également sortie en novembre. Honnêtement, pas de grosses nouveautés sur un Framework qui commence à être mature.
A l’utilisation, nous pouvons noter des améliorations notable sur la rapide du build et sur les informations présentées.
Microsoft Q/A
Enfin, Microsoft lance son site de question / réponse dédié au .NET. Il va falloir qu’ils mettent les bouchées doubles avec des supports par leur développeur pour que la plateforme grossisse vite ! En face, il y a Stack Overflow qui est déjà très complet.
clemox
18 h 53 min le 11 October 2020 Tags : .Net Core ( 7 ), ASP.Net Core, Autorisation
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.
CookieAuthenticationDefaults
Utilisation d’un simple cookie
JwtBearerDefaults
Utilisation 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.
Utilisation de service tiers pour authentifier un utilisateur.
IISDefaults
Utilisation du serveur IIS (par Active Directory) avec session gérée dans le processus IIS (In-Proc)
IISServerDefaults
Utilisateur 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).
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é !
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 !
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 ;))
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 : 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.
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.
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 ».
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.
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.
Il est souvent difficile de télécharger un fichier depuis une URL ou un BLOB avec Angular. Il existe plusieurs librairies dont FileSaver.JS qui permettent cela.
Elles sont compatibles avec Angular mais n’ont pas été écrites pour Angular. Cela fait une différence car le compilateur a plus de mal à optimiser les bundles. J’ai donc décidé de créer mon propre projet Angular de téléchargement de fichier, et de le partager avec la communauté.
Le code source est entièrement sous mon GitHub, et le package est téléchargeable sous NPM. L’utilisation est très simple et est compatible Chromium, Firefox et IE11.
La version 10 du Framework de développement Web est sortie. Pas de grosse nouveautés au programme, il s’agit surtout d’un ensemble de correction de problèmes, d’optimisation et de ménage des éléments dépréciés.
En développant des applications Angular, je me suis confronté à un problème :
Comment paramétrer mon application selon l’environnement ?
La réponse du Framework est bien entendu le fichier « environment.ts » avec son « environment.prod.ts » qui gère les variables selon le mode de compilation. Ce fonctionnement n’est vraiment pas pratique. En effet, les éléments de configuration se trouvent dans un fichier TS qui est compilé et donc non modifiable.
Dans une bonne intégration, le package compilé ne change pas et la configuration est faite selon l’environnement. Hors, à minima, l’URL de la Web API utilisée par le site Web Angular change selon l’environnement. De même, l’application n’a pas à être redéployée parce qu’un paramètre est changé.
2. Conception de la solution
J’ai du développer un système particulier permettant de gérer la configuration du site Web même après la compilation de l’application.
L’idée est la suivante :
Angular utilise toujours le fichier « environment.ts » pour accéder aux paramètres dans le développement.
Un fichier JSON décrivant les paramètres du fichier « environment.ts » est chargé au démarrage du site Web Angular
Ainsi, il suffira de modifier le fichier JSON de configuration mais une prise à chaud dans l’application sans nécessité de recompilation.
3. Implémentation de la solution
Premièrement, il faut créer un service permettant de lire le fichier JSON et l’injecter dans l’objet « environment ».
Fichiers « environment.ts » et « environment.prod.ts » Le fichier TS décrivant les paramètres de l’application et accessible lors du développement. Les propriétés ont des valeurs par défaut.
Fichier « config.json » Le fichier de configuration personnalisé de l’application qui reprend les propriétés de l’objet d’environnement. Il est créé au même niveau que le fichier « main.ts » du répertoire « src ».
Fichier « configuration-helper.ts » Le fichier de service est fait pour être mis dans une librairie de service.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
@Injectable()
export class ConfigurationHelper {
constructor(private http: HttpClient) { }
// Charger les informations du fichier de configuration
public load(environmentObject: object, path: string): Promise<any>
{
return this.http.get<any>(path).pipe(map(data => {
if (data !== null && data !== undefined) {
Object.keys(data).forEach(propertyData => {
if (Object.keys(environmentObject).findIndex(x => x === propertyData) !== -1) {
data[propertyData] = environmentObject[propertyData];
}
});
}
return data;
})).toPromise();
}
}
La méthode est très générique. Elle recherche les propriétés du fichier JSON qui correspondent aux propriétés de l’objet de l’environnement, et lui affecte la valeur.
Fichier « app-module.ts » Le fichier est le module d’entrée de l’application.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { environment } from 'src/environments/environment';
...
// Déclaration de la fonction d'initialisation de la configuration
export function initConfig(configService: ConfigurationHelper) {
return () => configService.load(environment, 'config.json');
}
@NgModule({
imports: [...],
declarations: [...],
// Déclaration de l'appel à la méthode "initConfig" au démarrage de l'application
providers: [
{
provide: APP_INITIALIZER, useFactory: initConfig, deps: [ConfigurationHelper], multi: true },
],
bootstrap: [AppComponent]
})
export class AppModule { }
L’implémentation est en 2 parties :
1 : la méthode d’appel de notre service en passant en paramètre l’objet de l’environnement et le lien vers le fichier « config.json ».
2 : déclaration d’une factory appelant la méthode au démarrage de l’application
3. Tests
Il n’y a plus qu’à tester que le tout fonctionne. Il vous suffit de lancer l’application Angular via le « ng serve » ou la compiler pour la déployer sur un serveur.
Les valeurs du fichiers « config.json » seront prises à chaud. Il faut quand même un rafraichissement du navigateur 😉
Angular permet de créer facilement des composants Web très puissant grâce à la gestion de template. C’est ce que nous allons utiliser pour créer une liste complexe.
1. Le besoin
En HTML, la gestion des listes est très basique et le rendu est (moche) non personnalisable. Le simple code suivant affiche une liste dont les éléments sont du simple texte non personnalisable.
Le besoin est par exemple d’avoir une liste pour laquelle le logo de la société est affichée à gauche du nom, ou pouvoir avoir une présentation complexe.
2. La conception
Pour implémenter notre liste, nous allons créer un composant spécialisé qui va gérer le menu de la liste. Le développeur gérera lui-même le tag de l’input, le composant viendra s’accrocher dessus.
Cette solution est très simple à mettre en oeuvre et laisse le développeur libre d’ajouter du style ou des erreurs à l’input.
3. L’implémentation
Nous créons un composant classique Angular avec les fichiers : – Le contrôleur : list-view.component.ts – Le template : list-view.component.html – Le style : list-view.component.less
3.1. Définition du contrôleur
Toutes les propriétés sont décrites en commentaire, mais rien de très compliqué 😉
Component({
selector: 'list-view',
templateUrl: 'list-view.component.html'
})
export class ListViewComponent implements OnInit {
constructor() { }
// le lien vers l'input
@Input() field: HTMLInputElement;
// liste des items de la liste
@Input() items: any[];
// nom de la propriété de l'item qui sera affichée dans l'input
@Input() propertyDisplay = 'text';
// valeur sélectionnée entrée / sortie
@Input() ngModel: any;
@Output() ngModelChange = new EventEmitter<any>();
// Est-ce que le menu de la liste est visible ?
public visible = false;
//
// Initialisation
ngOnInit() { }
}
Ensuite, nous allons appliquer un style sur notre « input » pour qu’il ressemble le plus possible à un « select » et restreindre les possibilité d’écriture.
En résumé, l’input est mis en lecture seule avec une image de flèche positionnée à droite. L’image utilisée est la suivante.
Enfin, nous allons gérer la visibilité du menu de la liste. L’événement « click » de l’input est accroché et on inverse la valeur de visibilité de menu de la liste.
Nous ajoutons également la méthode permettant de sélectionner un élément dans la liste et de le renvoyer vers le composant parent. Egalement, nous forçons l’affichage du texte dans l’input..
public select(model: any) {
this.ngModelChange.emit(model); // envoyer la sélection vers le parent
this.field.value = model[this.propertyDisplay];
this.visible = false; // fermeture du menu
}
3.2 Le template
Cette partie est plus complexe car elle intègre la notion de template personnalisé. Voici le début de l’implémentation :
<div class="list-view" *ngIf="visible">
<div *ngFor="let i of items" (click)="select(i)">
</div>
</div>
Le HTML décrit notre menu dont l’affichage dépend du paramètre « visible ». Lui-même contient la liste des item générés par un classique « *ngFor ». Au clic sur l’un des items, la méthode « select » est appelée.
Pour gérer notre template personnalisé, nous allons ajouter la déclaration suivante dans la partie contrôleur :
Cette ligne permet déclarer une variable « template » et donne accès à l’élément déclaré dans le HTML du composant. Dans l’exemple ci-dessous, cela correspond à la balise « <ng-template> » qui est de type Angular « TemplateRef ».
Retournons maintenant dans le template de notre composant pour le compléter :
<div *ngFor="let i of items" (click)="select(i)">
<ng-container *ngTemplateOutlet="template; context:{item: i}">
</ng-container>
</div>
La nouvelle ligne « ng-container » utilise l’attribut « *ngTemplateOutlet » pour injecter le « template » définie dans la partie contrôleur. De plus, la propriété « context » permet de décrire un objet contextuel au template. Nous y déclarons une variable « item » à laquelle l’item courant est affecté.
3.3 Le style
La dernière partie concerne le style de notre menu qui est décrit dans un fichier LESS.
Nous avons enfin défini notre liste personnalisée, il ne reste plus qu’à l’utiliser 😀 Imaginons un composant Angular dans lequel nous avons défini une liste de sociétés avec leur icône. Le but est d’en sélectionnée une.
Déclaration dans le contrôleur Nous déclarons la valeur qui sera sélectionnée, ainsi que la liste des éléments sélectionnables : un id, un texte et une icône (url externe).
Déclaration dans le template C’est la partie qui nous intéresse le plus car elle décrit notre template personnalisé. Dans la balise « ng-template », nous créons une variable « oneItem » grâce à l’attribut « let-oneItem » à laquelle nous affectons « item » qui est la valeur définie dans le « context » de notre contrôleur (voir 3.2).
Le début de nom d’attribut « let- » est reconnu par Angular et permet de déclarer une variable locale. Nous pouvons ainsi utiliser la variable « oneItem » pour accéder à l’objet d’une société de la liste « itemList » dans le template « ng-template ». Cela nous permet de donner une forme personnalisée à notre liste.
Pour des raisons de simplicité, j’ai directement intégré le CSS dans le template, mais il est plus propre de le sortir dans un fichier CSS global à part.
Le résultat est beaucoup plus sympa que la version d’origine 🙂
5. Quelques améliorations
Cet exemple vous donne la base pour créer des template personnalisé avec répétition. Dans l’exemple de la liste, il manque quelques points pour la rendre plus propre.
Fermer la liste si l’on clique à côté du menu Il faut ajouter le code suivant qui vérifie si l’utilisateur clique en dehors du menu.
<div #menuList>
<div class="list-view" *ngIf="visible">
<div *ngFor="let i of items" (click)="select(i)">
<ng-container *ngTemplateOutlet="template; context:{item: i}"></ng-container>
</div>
</div>
</div>
Sélectionner une valeur initiale Il faut ajouter le code suivant qui sélectionne la valeur par défaut à l’initialisation.
// nom de la propriété de l'item qui sera affichée dans l'input
@Input() propertyId = 'id';
// sélectionner la 1ère valeur par défaut
@Input() selectFirstValue = true;
export class ListViewComponent implements OnInit, OnChanges {
...
ngOnChanges() {
this.initializeDefaultSelection();
}
private initializeDefaultSelection() {
if (this.items && this.items.length > 0) {
let item = null;
if (this.value) {
item = this.items.find(x =>
x[this.propertyId] === this.value[this.propertyId]);
} else if (this.selectFirstValue) {
item = this.items[0];
}
if (item !== null) this.select(item);
}
}
}
Il est encore possible d’aller plus loin : – Remplacer l’input par l’affichage du template personnalisé – Permettre de taper du texte dans l’input pour rechercher les éléments – Faire un appel asynchrone pour récupérer une liste d’élément depuis un serveur – Modifier la largeur du menu pour qu’il est la même que l’input – …
6. Code source complet
Pour finir, voici le code source complet des 3 fichiers.
list-view.component.ts
import {
Component, ContentChild, ElementRef, EventEmitter,
HostListener, Input, OnChanges, OnInit, Output,
TemplateRef, ViewChild,
} from '@angular/core';
@Component({
selector: 'list-view',
templateUrl: 'list-view.component.html',
styleUrls: ['./list-view.component.less']
})
export class ListViewComponent implements OnInit, OnChanges {
constructor() { }
//// les entrées / sorties
// le lien vers l'input
@Input() field: HTMLInputElement;
// liste des items de la liste
@Input() items: any[];
// nom de la propriété de l'item qui sera affichée dans l'input
@Input() propertyId = 'id';
// nom de la propriété de l'item qui sera affichée dans l'input
@Input() propertyDisplay = 'text';
// sélectionner la 1ère valeur par défaut
@Input() selectFirstValue = true;
// valeur sélectionnée entrée / sortie
@Input() value: any;
@Output() valueChange = new EventEmitter<any>();
//
// Est-ce que le menu de la liste est visible ?
public visible = false;
@ViewChild('menuList', { read: ElementRef, static: true }) menuList: ElementRef;
@ContentChild(TemplateRef, { static: false }) template;
//
ngOnInit() {
if (this.field) {
this.stylyzeField(this.field);
this.field.addEventListener('click', () => {
this.visible = !this.visible;
});
}
}
ngOnChanges() {
this.initializeDefaultSelection();
}
@HostListener('document:click', ['$event'])
clickedOutside(event: Event) {
const elt = event.target as Element;
if (elt !== this.field) {
if (this.menuList && this.menuList.nativeElement !== event.target && !this.menuList.nativeElement.contains(elt)) {
this.visible = false;
}
}
}
public select(model: any) {
this.valueChange.emit(model);
this.field.value = model[this.propertyDisplay];
this.visible = false;
}
//
private stylyzeField(field: HTMLInputElement) {
field.setAttribute('autocomplete', 'off');
field.setAttribute('readonly', 'readonly');
field.style.background = '#FFFFFF url(/Styles/images/arrow-bottom.png) no-repeat right';
field.style.backgroundSize = '23px 25px';
field.style.cursor = 'pointer';
field.style.paddingRight = '28px';
field.style.userSelect = 'none';
field.addEventListener('select', () => {
field.selectionStart = field.selectionEnd;
});
}
private initializeDefaultSelection() {
if (this.items && this.items.length > 0) {
let item = null;
if (this.value) {
item = this.items.find(x => x[this.propertyId] === this.value[this.propertyId]);
} else if (this.selectFirstValue) {
item = this.items[0];
}
if (item !== null) this.select(item);
}
}
}
list-view.component.html
<div #menuList>
<div class="list-view" *ngIf="visible">
<div *ngFor="let i of items" (click)="select(i)">
<ng-container *ngTemplateOutlet="template; context:{item: i}"></ng-container>
</div>
</div>
</div>
Il s’agit d’une version majeure depuis les 3 dernières années. En effet, la version active IVY, le nouveau moteur de rendu du Framework.
Faire la mise à jour
Cette nouvelle version nécessite en prérequis d’installer NodeJS 10 au minimum.
La page de migration d’Angular décrit bien la procédure à suivre. Je conseille l’exécution des commandes suivantes :
ng update @angular/cli@8 @angular/core@8
ng update @angular/cli @angular/core
npm -g update @angular/cli
Cela prend un peu de temps pour tout mettre à jour.
Quels sont les nouveautés ?
Les nouveautés de la version sont décrites sur le blog d’Angular. Les principales informations sont les suivantes. Je vous invite aussi à lire les API abandonnées.
IVY Le nouveau moteur de rendu du Framework inclut plein d’améliorations sans pour autant modifier le code de l’application, notamment :
Les bundles sont plus petits
Meilleurs performances pendant les tests
L’objet « ng » permet d’appeler des méthodes du composant pour le debug
Meilleur affichage des erreurs
TypeScript 3.7 La dernière version du langage est utilisée par Angular 9. Je vous invite à consulter les nouveautés des versions 3.6 et 3.7.
Le point que je trouve le plus marquant est l’opérateur de chaîne optionnel « ? ». Il est maintenant possible d’écrire :
let a = monObjet?.maPropriete1?.maPropriete2
let a = monTableau?.[1]
// => renvoi "undefined" si l'élément n'est pas accessible
Dans la pratique ?
Concrètement, j’ai fait la migration sur l’un de mes projets Angular 8.
Je n’ai eu aucune erreur de compilation après la migration du projet. Suite à compilation, le bundle est passé de 3,15 Mo à 2,92 Mo, soit une diminution de 7%.