Aller au contenu principal

31-5 Gestion de l'état d'authentification

On assigne le jeton retourné à une clé du LocalStorage, mais Blazor n'a aucune façon de savoir que ce jeton est présent et encore moins qu'il contient des informations de connexion.

Pour gérer l'état d'authentification et supporter l'autorisation des composants, Blazor propose le concept de Authentication State Provider.

L'Authentication State Provider a pour rôle principal de déterminer si un utilisateur est authentifié ou non. Il fournit des informations sur l'utilisateur actuel, telles que son identité et ses rôles. Ce service permet également de mettre à jour l'état d'authentification lorsqu'un utilisateur se connecte, se déconnecte ou s'inscrit.

Le principe est le suivant:

  1. L'application Blazor effectue une requête pour obtenir l'état d'authentification courant en appelant GetAuthenticationStateAsync().
  2. L'Authentication State Provider retourne un objet Task<AuthenticationState> qui représente l'état d'authentification courant.
  3. L'application Blazor peut alors utiliser cet objet pour déterminer si l'utilisateur est authentifié ou non et personnaliser l'expérience utilisateur en conséquence.

Pour fonctionner avec le jeton, on doit créer notre propre AuthenticationStateProvider qui héritera de la classe AuthenticationStateProvider

Créer ApiTokenAuthenticationStateProvider

  1. Sous le projet Admin -> Add -> Directory

  2. Nommez le dossier Providers

  3. Sous Admin/Providers -> Add -> Class/Interface

  4. Nommez la classe ApiTokenAuthenticationStateProvider

  5. Faites hériter la classe de AuthenticationStateProvider et implémentez la fonction obligatoire:

    Snowfall.Web.Admin/Providers/ApiTokenAuthenticationStateProvider.cs
    public class ApiTokenAuthenticationStateProvider : AuthenticationStateProvider
    {
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
    throw new NotImplementedException();
    }
    }

    Nous verrons le détail plus loin.

Ajouter le support pour notre implémentation de AuthenticationStateProvider

Dans Program.cs, on doit ajouter aux services les fonctionnalités de base fournies par Blazor (AuthorizationCore):

Snowfall.Web.Admin/Program.cs
// Authorization
builder.Services.AddAuthorizationCore();

Et ensuite, dans notre fichier d'injection de dépendances personnalisées, on peut y injecter notre AuthenticationStateProvider.

Snowfall.Web.Admin/Configurations/InjectionDependancesConfig.cs
public static class InjectionDependancesConfig
{
public static IServiceCollection EnregistrerServices(this IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));

services.AddScoped<EvenementHttpClient>();
services.AddScoped<VilleHttpClient>();
services.AddScoped<UploadHttpClient>();
services.AddScoped<AuthHttpClient>();
services.AddScoped<AuthenticationStateProvider, ApiTokenAuthenticationStateProvider>();

return services;
}
}
info

De cette façon, lorsque Blazor demandera le AuthenticationStateProvider, c'est ApiTokenAuthenticationStateProvider qui sera retourné, soit notre implémentation!

Ajouter le support pour les composants d'autorisation Blazor

Blazor s'appuie sur quelques composants pour faciliter l'intégration de l'autorisation et afin d'obtenir le support pour ces dernières dans l'application, il faut ajouter au fichier _Imports.razor la référence suivante:

Snowfall.Web.Admin/_Imports.razor
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Snowfall.Web.Admin
@using Snowfall.Web.Admin.Layout
@using Snowfall.Web.Admin.Components
@using Microsoft.AspNetCore.Components.Forms
@using BlazorBootstrap;
@using Microsoft.AspNetCore.Components.Authorization

Ajouter <CascadingAuthenticationState> et <AuthorizeRouteView>

Finalement, pour que l'état d'authentification soit disponible partout dans l'application, on peut l'ajouter comme un Cascading parameter, c'est-à-dire qu'il se retrouve automatiquement dans tous les composants.

Pour ce faire, on englobe tout de App.razor dans la balise <CascadingAuthenticationState>. De plus <RouteView> doit être changé pour <AuthorizeRouteView>.

info

CascadingAuthenticationState est un composant intégré à Blazor qui permet de partager l'état d'authentification d'un utilisateur dans l'ensemble de l'application.

Il fonctionne avec le système d'authentification de Blazor, qui prend en charge divers scénarios d'authentification, tels que l'authentification basée sur les cookies, l'authentification basée sur les jetons, ou même des solutions personnalisées.

Snowfall.Web.ClientAdmin/App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

Exécuter le projet et tester via un utilisateur anonyme

Si vous exécutez le projet, vous devriez voir quelque chose comme ceci:

Imgur

Au fond, au chargement de l'application, Blazor utilise ApiTokenAuthenticationStateProvider pour obtenir l'état de l'authentification. Comme on lance une exception NotImplementedException(), c'est pourquoi on obtient une erreur dans le navigateur.

Pour retourner un utilisateur anonyme, on peut créer un ClaimsIdentity vide:

Snowfall.Web.Admin/Providers/ApiTokenAuthenticationStateProvider.cs
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var utilisateurAnonyme = new ClaimsPrincipal(new ClaimsIdentity());
return await Task.FromResult(new AuthenticationState(utilisateurAnonyme));
}

Cela satisfera le AuthenticationState pour l'instant.

Protéger une page

Pour protéger une page, on peut utiliser l'attribut [Authorize]. Par exemple, pour protéger la page d'accueil (index):

Snowfall.Web.Admin/Pages/Home.razor
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

Si vous tentez de charger la page d'accueil, vous aurez un message générique Not authorized.

Il est possible de modifier ce message dans App.razor:

Snowfall.Web.Admin/App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<p>Désolé, vous n'avez pas accès à cette page.</p>
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>

Le message sera différent cette fois:

http://localhost:4200

Rediriger vers la page de connexion si non connectée

Plutôt que d'afficher un message générique, on peut prendre la liberté de rediriger l'utilisateur vers l'écran de connexion.

On créera un composant qui fera cd travail et plutôt que d'afficher le message d'erreur, le composant de redirection sera utilisée.

  1. Sous le dossier Admin/Components -> Add -> Blazor Component
  2. Sélectionnez Component comme type
  3. Nommez le composant RedirectConnexion
  4. Vous pouvez utiliser le code suivant qui redirigera un utilisateur non connecté vers l'écran de connexion:
    Snowfall.Web.tAdmin/Components/RedirectConnexion.razor
    @inject NavigationManager Navigation

    @code {
    protected override async Task OnInitializedAsync()
    {
    Navigation.NavigateTo("/connexion");
    }
    }
  5. Pour finalement utiliser cette composante dans la section <NotAuthorized> de App.razor.
    Snowfall.Web.Admin/App.razor
    <CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
    <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
    <NotAuthorized>
    <RedirectConnexion />
    </NotAuthorized>
    </AuthorizeRouteView>
    <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
    </Found>
    <NotFound>
    <PageTitle>Not found</PageTitle>
    <LayoutView Layout="@typeof(MainLayout)">
    <p role="alert">Sorry, there's nothing at this address.</p>
    </LayoutView>
    </NotFound>
    </Router>
    </CascadingAuthenticationState>

Cacher des éléments en fonction de l'authentification

Peut-être avez-vous remarqué que même si l'utilisateur n'est pas connecté, la barre de navigation contenant les liens de menu est affichée. Il est fort probable qu'on ne veuille pas afficher l'élément de menu Gestion des événements si l'utilisateur n'est pas connecté!

Pour ce faire, il est possible d'utiliser <AuthorizeView> pour encapsuler des éléments d'une vue qu'on ne veut rendre accessible qu'aux utilisateurs connectés.

Par exemple, dans la Navbar:

Snowfall.Web.Admin/Components/Navbar.razor
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm border-bottom mb-5">
<div class="container-fluid">
<NavLink href="" class="navbar-brand">Snowfall</NavLink>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<AuthorizeView>
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<NavLink class="nav-link" ActiveClass="active" href="evenements">Gestion des événements</NavLink>
</li>
</ul>
</AuthorizeView>
</div>
</div>
</nav>
</header>

Mieux!

http://localhost:4200

Utiliser le jeton et le Local Storage pour l'état d'authentification

Dernier arrêt, modifier ApiTokenAuthenticationStateProvider pour qu'il utilise le jeton et les informations de l'utilisateur contenu dans ce dernier.

Récupérer le jeton via GetAuthenticationStateAsync

Nous allons modifier la fonction GetAuthenticationStateAsync pour qu'elle récupère le jeton du local storage, crée le AuthenticationState et assigne à l'en-tête HTTP Authorization le jeton pour toutes les requêtes d'API subséquentes.

  1. Injecter LocalStorage et HttpClient
    Snowfall.Web.Admin/Providers/ApiTokenAuthenticationStateProvider.cs
    public class ApiTokenAuthenticationStateProvider : AuthenticationStateProvider
    {
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;

    public ApiTokenAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage)
    {
    _httpClient = httpClient;
    _localStorage = localStorage;
    }

    //...
  2. Récupérer le jeton du Local Storage. Si ce dernier est null, on retourne un authentication state anonyme (utilisateur non connecté).
    Snowfall.Web.ClientAdmin/Providers/ApiTokenAuthenticationStateProvider.cs
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
    var jetonLocalStorage = await _localStorage.GetItemAsync<string>("authToken");

    if (string.IsNullOrWhiteSpace(jetonLocalStorage))
    {
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }
    }
  3. On peut ensuite lire le jeton à l'aide de la classe utilitaire JwtSecurityTokenHandler()
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
    var jetonLocalStorage = await _localStorage.GetItemAsync<string>("authToken");

    if (string.IsNullOrWhiteSpace(jetonLocalStorage))
    {
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }

    JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
    JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jetonLocalStorage);
    }
  4. On assigne à l'en-tête Authorization du client HTTP le jeton comme valeur par défaut. Comme il s'agit d'un singleton, partout où on utilisera le client HTTP, le jeton sera contenu dans l'en-tête des requêtes.
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
    var jetonLocalStorage = await _localStorage.GetItemAsync<string>("authToken");

    if (string.IsNullOrWhiteSpace(jetonLocalStorage))
    {
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }

    JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
    JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jetonLocalStorage);

    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", jetonLocalStorage);
    }
  5. Finalement, le AuthenticationState est retourné avec les claims contenus dans le jeton!
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
    var jetonLocalStorage = await _localStorage.GetItemAsync<string>("authToken");

    if (string.IsNullOrWhiteSpace(jetonLocalStorage))
    {
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
    }

    JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
    JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jetonLocalStorage);

    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", jetonLocalStorage);

    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(jwtToken.Claims, "jwt")));
    }

Déjà, si vous aviez un jeton valide dans le local storage et que vous exécutez l'application, vous aurez accès au contenu!

attention

Pour la suite, assurez-vous de ne pas avoir de jeton de sauvegardé dans votre local storage de navigateur.

Vous pouvez supprimer manuellement ce dernier à partir des outils pour développeurs du navigateur.

Imgur

Rafraichir l'état d'authentification lors de la connexion

Lors de la connexion, il faut notifier ApiTokenAuthenticationStateProvider que l'état de la connexion a changé. Pour ce faire, on créera une petite fonction d'aide dans ApiTokenAuthenticationStateProvider appelée RafraichirAuthenticationState qui utilisera la fonction protégée NotifyAuthenticationStateChanged de la classe.

Snowfall.Web.Admin/Providers/ApiTokenAuthenticationStateProvider.cs
public void RafraichirAuthenticationState()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}

Ensuite, il ne reste qu'à utiliser cette fonction lorsque la connexion est un succès.

  1. Injecter AuthenticationStateProvider et NavigationManager dans le composant Connexion
    Snowfall.Web.Admin/Pages/Auth/Connexion.razor
    @inject AuthenticationStateProvider AuthenticationStateProvider;
    @inject NavigationManager NavigationManager;
  2. Notifier lors du changement d'état et rediriger vers la page d'accueil.
    async Task TraiterConnexion()
    {
    var resultatConnexionDto = await AuthClient.Connexion(connexionDto);

    if (resultatConnexionDto is not null)
    {
    await LocalStorage.SetItemAsStringAsync("authToken", resultatConnexionDto.Token);
    (AuthenticationStateProvider as ApiTokenAuthenticationStateProvider)!.RafraichirAuthenticationState();
    NavigationManager.NavigateTo("/");
    }
    else
    {
    alertMessage = Localizer["Erreur.Auth"];
    }
    }

Voilà! Il ne vous reste plus qu'à tester. Il devrait vous être possible de vous authentifier pour ensuite être redirigé vers la page d'accueil.