Authentification

AuthStateProvider

Pour gérer l'authentification dans Blazor, nous allons utiliser un AuthStateProvider. C'est une interface qui permet de gérer l'état de l'authentification dans notre application.

Créez un nouveau fichier AuthProvider.cs dans le dossier Services.

namespace MonFront.Services;

public class AuthProvider : AuthenticationStateProvider
{
    public AuthProvider()
    {
    }
}

On va injecter une instance de ProtectedLocalStorage dans le constructeur pour stocker l'état de l'authentification. ProtectedLocalStorage est une classe qui permet de stocker des données de manière sécurisée dans le navigateur de l'utilisateur. Nous allons l'utiliser pour stocker le token JWT de l'utilisateur lorsque celui-ci se connecte. Cele permet de garder l'utilisateur connecté même s'il rafraichit la page ou s'il ferme le navigateur.

private readonly ProtectedLocalStorage _sessionStorage;

public AuthProvider(ProtectedLocalStorage protectedSessionStorage)
{
    _sessionStorage = protectedSessionStorage;
}

Pour l'utiliser voici la page du cours : Cours

GetAuthenticationStateAsync

Pour récupérer l'état de l'authentification AuthenticationStateProvider doit implémenter la méthode GetAuthenticationStateAsync.

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var claim = new ClaimsPrincipal(new ClaimsIdentity()); // Créer un objet ClaimsPrincipal vide == utilisateur non connecté
            return new AuthenticationState(claim); // Retourne l'état de l'authentification
        }

Login

Créeons une méthode Login qui prend en paramètre un User et un token JWT et qui les stockent dans le ProtectedLocalStorage. Puis qui génère un ClaimsPrincipal à partir de l'utilisateur et qui notifie le changement d'état de l'authentification.

public async Task Login(User user, string token)
{
    await _sessionStorage.SetAsync("User", user); // Stocke l'utilisateur
    await _sessionStorage.SetAsync("Token", token); // Stocke le token
    ClaimsPrincipal claim = GenerateClaimsPrincipal(user); // Génère un ClaimsPrincipal
    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claim))); // Notifie le changement d'état de connexion
}

public ClaimsPrincipal GenerateClaimsPrincipal(User user)
{
    var claims = new[]
    {
        new Claim("Id", user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Pseudo),
        new Claim(ClaimTypes.Role, user.Role.ToString())
    };
    ClaimsIdentity identity = new ClaimsIdentity(claims, "custom");
    ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(identity);
    return claimsPrincipal;
}

Logout

Créeons une méthode Logout qui supprime l'utilisateur et le token du ProtectedLocalStorage et qui notifie le changement d'état de l'authentification.

public async Task Logout()
{
    await _sessionStorage.DeleteAsync("User"); // Supprime l'utilisateur
    await _sessionStorage.DeleteAsync("Token"); // Supprime le token
    var claimDisconnected = new ClaimsPrincipal(new ClaimsIdentity()); // Créer un objet ClaimsPrincipal vide == utilisateur non connecté
    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimDisconnected))); // Notifie le changement d'état de connexion
}

Modifier GetAuthenticationStateAsync

Modifions la méthode GetAuthenticationStateAsync pour qu'elle retourne un ClaimsPrincipal si l'utilisateur est connecté.

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var user = await _sessionStorage.GetAsync<User>("User"); // Récupère l'utilisateur
    if (user.Value != null) // Si on a un utilisateur dans le local storage
    {
        var claim = GenerateClaimsPrincipal(user.Value); // Génère un ClaimsPrincipal
        return new AuthenticationState(claim); // Retourne l'état de l'authentification
    }
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); // Retourne un utilisateur non connecté
}

Utilisation

Pour utiliser notre AuthProvider dans notre application, nous allons l'injecter dans le Program.cs.

builder.Services.AddScoped<AuthenticationStateProvider, AuthProvider>();

Si nous voulons utiliser notre AuthProvider dans une page, nous allons l'injecter dans le constructeur de la page.

@page "/Login"
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthStateProvider

@code {

    // login du user sur le authProvider
    private async Task LoginAction(UserInfo userInfo)
    {
        var userAndJwt = await UserService.Login(userInfo.Pseudo, userInfo.Password); // Appel à notre API
        if (userAndJwt != null) // Si l'API nous retourne un utilisateur
        {
            await ((AuthProvider)AuthStateProvider).Login(userAndJwt.User, userAndJwt.Token); // Connexion
            NavigationManager.NavigateTo("/"); // Redirection vers la page d'accueil
        }
    }

    // Redirection si l'utilisateur est déjà connecté
    protected override async Task OnInitializedAsync()
    {
        var returnUrl = GetQueryParm("ReturnUrl");
        if (returnUrl != "")
        {
            var isLogged = await AuthStateProvider.GetAuthenticationStateAsync();
            if (isLogged.User.Identity is not null && isLogged.User.Identity.IsAuthenticated)
            {
                NavigationManager.NavigateTo(returnUrl);
            }
        }
    }
    
    string GetQueryParm(string parmName)
    {
        var uriBuilder = new UriBuilder(NavigationManager.Uri);
        var q = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
        return q[parmName] ?? "";
    }
    // Fin de la redirection
    

}

Activation de l'authentification

Pour activer l'authentification dans notre application, nous allons ajouter les lignes suivantes dans le Program.cs

builder.Services.AddAuthentication(o =>
  {
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  })
  .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
  {
    options.Cookie.Name = "auth_cookie";
    options.LoginPath = "/Login"; // Redirection vers la page de connexion
  });

builder.Services.AddAuthenticationCore();

...

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

Il faudra également modifier le ficheir Components/Router.razor pour indiquer à notre application de vérifier l'authentification avant d'afficher une page.

On change AuthorizeRouteView à la place de RouteView.

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

Authentification des pages

Pour sécuriser une page, nous allons utiliser l'attribut [Authorize] comme dans l'API.

@page "/Admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Page Admin</h1>

Récupérer l'utilisateur connecté

Pour récupérer l'utilisateur connecté, nous allons utiliser la méthode GetAuthenticationStateAsync de AuthenticationStateProvider.

@inject AuthenticationStateProvider AuthenticationStateProvider

@code {
    private bool _isLogged = false;

    private async Task isUserConnected()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        _isLogged = user.Identity?.IsAuthenticated ?? false;
    }

    private async Task<string> getUsername()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        return user.FindFirst(ClaimTypes.Name).Value;
    }
}

AuthorizeView

Affichage en fonction de l'authentification

Voici comment afficher un bouton de connexion ou de déconnexion en fonction de l'authentification.

<AuthorizeView>
    <Authorized>
        <button @onclick="Logout">Déconnexion</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="Login">Connexion</button>
    </NotAuthorized>
</AuthorizeView>

Affichage en fonction du rôle

Voici comment afficher un bouton en fonction du rôle de l'utilisateur.

Ici on affiche un bouton seulement si l'utilisateur a le rôle Admin.

<AuthorizeView Roles="Admin">
    <Authorized>
        <button @onclick="AdminAction">Action Admin</button>
    </Authorized>
</AuthorizeView>

Utiliser le context

Quand vous utilisez le composant AuthorizeView vous avez accès a une variable context qui contient des informations sur l'utilisateur connecté.

<AuthorizeView>
    <Authorized>
        <p>Bonjour @context.User.Identity.Name</p>
        <p>Votre rôle est @context.User.FindFirst(ClaimTypes.Role).Value</p>
    </Authorized>
</AuthorizeView>