🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

🔐 TP 11 : Sécurité, Authentification & Autorisation (Identity)

Module : Programmation .Net C#

Durée : 2h

Objectif : Intégrer ASP.NET Core Identity pour sécuriser l'application. Créer une architecture hybride (Blazor + Minimal API) adaptée à .NET 10 pour la gestion des Cookies de sécurité. Sécuriser l'interface et les pages selon le rôle (Admin).

Pré-requis :

  • Avoir terminé le TP 10.
  • Comprendre le fonctionnement de Entity Framework Core et des Migrations.

Activité 1 : Préparation de la Base de Données (20 min)

ASP.NET Core Identity possède ses propres tables SQL prêtes à l'emploi (pour gérer les mots de passe hashés, les rôles, etc.).

  1. Installation du package :
    Dans le terminal de VS Code, installez l'extension EF Core pour Identity :
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

2. Modification du DbContext :
Ouvrez Data/AppDbContext.cs. Changez l'héritage de DbContext vers IdentityDbContext<IdentityUser>.

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; // Nécessaire
using Microsoft.EntityFrameworkCore;
using DashboardData.Models;

namespace DashboardData.Data;

// On hérite maintenant de IdentityDbContext !
public class AppDbContext : IdentityDbContext<IdentityUser>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<SensorData> Sensors { get; set; }
    public DbSet<Location> Locations { get; set; }
}

3. Génération des tables Identity :
Exécutez ces commandes pour créer les tables de sécurité dans votre base de données :

dotnet ef migrations add AddIdentitySystem
dotnet ef database update

Activité 2 : Configuration Globale & Seeding (30 min)

Nous allons configurer le moteur d'Identity, propager l'état de sécurité dans Blazor, et créer un Administrateur par défaut.

  1. Importation globale (_Imports.razor) :
    Ouvrez le fichier Components/_Imports.razor et ajoutez ces lignes à la fin. Cela évitera de les réécrire dans chaque page :
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

2. Propagation de l'état d'authentification (Routes.razor) :
Dans une application Blazor Web .NET 10, il faut dire au routeur d'écouter les changements de connexion. Ouvrez Components/Routes.razor et entourez le composant <Router> avec <CascadingAuthenticationState> :

<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly">
        <!-- Le reste de votre code RouteView, etc. reste inchangé à l'intérieur -->
    </Router>
</CascadingAuthenticationState>

3. Enregistrement des services (Program.cs) :
Ouvrez Program.cs. Avant la ligne var app = builder.Build();, ajoutez :

using Microsoft.AspNetCore.Identity;

// ... après builder.Services.AddDbContext(...) ...

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = false;
    options.Password.RequiredLength = 6;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

builder.Services.AddCascadingAuthenticationState();

4. Création du compte Administrateur (Seeding) :
Toujours dans Program.cs, juste après var app = builder.Build();, ajoutez ce code :

using (var scope = app.Services.CreateScope())
{
    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();

    if (!await roleManager.RoleExistsAsync("Admin"))
        await roleManager.CreateAsync(new IdentityRole("Admin"));

    if (await userManager.FindByEmailAsync("admin@data.com") == null)
    {
        var adminUser = new IdentityUser { UserName = "admin@data.com", Email = "admin@data.com" };
        var result = await userManager.CreateAsync(adminUser, "Admin123!");

        if (result.Succeeded)
            await userManager.AddToRoleAsync(adminUser, "Admin");
    }
}

Activité 3 : Les Endpoints d'Authentification (.NET 10) (25 min)

Explication vitale : Blazor Interactive (WebSocket) ne peut pas modifier les Cookies HTTP (erreur "Headers are read-only"). Les actions de sécurité doivent donc passer par des requêtes HTTP classiques (Minimal APIs).

  1. Dans Program.cs, ajoutez using Microsoft.AspNetCore.Mvc; tout en haut.
  2. Tout en bas du fichier, juste avant app.Run();, ajoutez ces routes :
// --- ENDPOINTS D'AUTHENTIFICATION (Hors WebSocket) ---

app.MapPost("/api/auth/login", async (
    [FromServices] SignInManager<IdentityUser> signInManager,
    [FromForm] string email, 
    [FromForm] string password) =>
{
    var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: false, lockoutOnFailure: false);
    
    if (result.Succeeded) return Results.Redirect("/dashboard");
    
    return Results.Redirect("/login?error=Identifiants+incorrects");
}).DisableAntiforgery(); 

app.MapPost("/api/auth/logout", async ([FromServices] SignInManager<IdentityUser> signInManager) =>
{
    await signInManager.SignOutAsync();
    return Results.Redirect("/");
}).DisableAntiforgery();

app.Run(); // Cette ligne doit toujours être la dernière du fichier !

Activité 4 : Les Interfaces de Connexion et Déconnexion (25 min)

  1. La page de Login (Components/Pages/Login.razor) :
    Créez ce fichier. C'est un formulaire HTML standard qui cible notre API. L'attribut data-enhance="false" empêche Blazor d'intercepter le clic en WebSocket.
@page "/login"

<div class="row justify-content-center mt-5">
    <div class="col-md-4 card shadow-sm p-4">
        <h3 class="text-center mb-4">Connexion</h3>
        
        @if (!string.IsNullOrEmpty(Error))
        {
            <div class="alert alert-danger">@Error</div>
        }

        <form method="post" action="/api/auth/login" data-enhance="false">
            <div class="mb-3">
                <label>Email</label>
                <input type="text" name="email" class="form-control" required />
            </div>
            <div class="mb-3">
                <label>Mot de passe</label>
                <input type="password" name="password" class="form-control" required />
            </div>
            <button type="submit" class="btn btn-primary w-100">Se connecter</button>
        </form>
    </div>
</div>

@code {
    [SupplyParameterFromQuery]
    public string? Error { get; set; }
}

2. La Barre Supérieure (Components/Layout/MainLayout.razor) :
Nous allons remplacer le lien statique "About" en haut à droite par notre bloc d'authentification dynamique.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <!-- On remplace le contenu de la top-row -->
        <div class="top-row px-4">
            <AuthorizeView Context="authState">
                <Authorized>
                    <span class="me-3">👤 @authState.User.Identity?.Name</span>
                    <!-- Formulaire POST obligatoire pour la déconnexion HTTP -->
                    <form method="post" action="/api/auth/logout" data-enhance="false" class="d-inline">
                        <button type="submit" class="btn btn-sm btn-outline-primary">Déconnexion</button>
                    </form>
                </Authorized>
                <NotAuthorized>
                    <a href="/login" class="btn btn-primary btn-sm">Se connecter</a>
                </NotAuthorized>
            </AuthorizeView>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

Activité 5 : Sécuriser l'Application (Autorisation) (20 min)

Maintenant, nous appliquons nos règles métiers : seul l'Admin peut ajouter ou modifier un capteur.

  1. Protéger le backend (La page) :
    Ouvrez Components/Pages/EditSensor.razor. Tout en haut, sous @page, ajoutez l'attribut exigeant le rôle Admin. Si un visiteur tape l'URL manuellement, il sera bloqué.
@attribute [Authorize(Roles = "Admin")]

2. Protéger le frontend (Cacher les boutons) :
Ouvrez Components/Pages/MyDashboard.razor. Entourez le bouton "Nouveau Capteur" avec la balise de vérification :

<AuthorizeView Roles="Admin">
    <Authorized>
        <a href="/edit-sensor" class="btn btn-success mb-3">+ Nouveau Capteur</a>
    </Authorized>
</AuthorizeView>

3. Test global :


🚀 Exercices d'application (En autonomie)

Exercice 1 : Sécuriser le tableau de données

Dans le composant Components/SensorTable.razor, les boutons "Éditer" et "Supprimer" de chaque ligne sont toujours visibles pour tout le monde (même s'ils provoquent une erreur au clic en cas de tentative).
Mission : Entourez ces boutons (à l'intérieur de la boucle @foreach, dans la balise <td>) avec <AuthorizeView Roles="Admin"> pour les cacher visuellement aux simples visiteurs.

Exercice 2 : KPI Confidentiel (Autorisation granulaire)

Dans le domaine de la Data, certains indicateurs financiers ou critiques ne doivent être vus que par la direction.
Mission : Dans MyDashboard.razor, la jauge (Radial Gauge) affichant la "Valeur Max Détectée" est considérée comme confidentielle.
Utilisez <AuthorizeView Roles="Admin"> autour de la <div class="col-md-4"> qui contient cette jauge pour que l'interface s'adapte dynamiquement : les visiteurs verront uniquement le graphique en barres, tandis que l'Admin verra le graphique ET la jauge.


🏁 Synthèse du TP 11

🔐 LAB 11: Security, Authentication & Authorization (Identity)

Module: .Net C# Programming

Duration: 2h

Objective: Integrate ASP.NET Core Identity to secure the application. Create a hybrid architecture (Blazor + Minimal API) suitable for .NET 10 to manage security Cookies. Secure the interface and pages according to the role (Admin).

Prerequisites:

  • Have completed LAB 10.
  • Understand Entity Framework Core and Migrations.

Activity 1: Database Preparation (20 min)

ASP.NET Core Identity has its own SQL tables ready to use (to manage hashed passwords, roles, etc.).

  1. Package Installation:
    In the VS Code terminal, install the EF Core extension for Identity:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

2. DbContext Modification:
Open Data/AppDbContext.cs. Change the inheritance from DbContext to IdentityDbContext<IdentityUser>.

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; // Necessary
using Microsoft.EntityFrameworkCore;
using DashboardData.Models;

namespace DashboardData.Data;

// We now inherit from IdentityDbContext!
public class AppDbContext : IdentityDbContext<IdentityUser>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<SensorData> Sensors { get; set; }
    public DbSet<Location> Locations { get; set; }
}

3. Identity Tables Generation:
Execute these commands to create security tables in your database:

dotnet ef migrations add AddIdentitySystem
dotnet ef database update

Activity 2: Global Configuration & Seeding (30 min)

We will configure the Identity engine, propagate the security state in Blazor, and create a default Administrator.

  1. Global Import (_Imports.razor):
    Open Components/_Imports.razor and add these lines at the end. This will avoid rewriting them in each page:
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

2. Authentication State Propagation (Routes.razor):
In a .NET 10 Blazor Web application, you must tell the router to listen to connection changes. Open Components/Routes.razor and surround the <Router> component with <CascadingAuthenticationState>:

<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly">
        <!-- The rest of your RouteView code, etc. remains unchanged inside -->
    </Router>
</CascadingAuthenticationState>

3. Services Registration (Program.cs):
Open Program.cs. Before the line var app = builder.Build();, add:

using Microsoft.AspNetCore.Identity;

// ... after builder.Services.AddDbContext(...) ...

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = false;
    options.Password.RequiredLength = 6;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

builder.Services.AddCascadingAuthenticationState();

4. Administrator Account Creation (Seeding):
Still in Program.cs, just after var app = builder.Build();, add this code:

using (var scope = app.Services.CreateScope())
{
    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();

    if (!await roleManager.RoleExistsAsync("Admin"))
        await roleManager.CreateAsync(new IdentityRole("Admin"));

    if (await userManager.FindByEmailAsync("admin@data.com") == null)
    {
        var adminUser = new IdentityUser { UserName = "admin@data.com", Email = "admin@data.com" };
        var result = await userManager.CreateAsync(adminUser, "Admin123!");

        if (result.Succeeded)
            await userManager.AddToRoleAsync(adminUser, "Admin");
    }
}

Activity 3: Authentication Endpoints (.NET 10) (25 min)

Vital explanation: Blazor Interactive (WebSocket) cannot modify HTTP Cookies ("Headers are read-only" error). Security actions must therefore pass through standard HTTP requests (Minimal APIs).

  1. In Program.cs, add using Microsoft.AspNetCore.Mvc; at the very top.
  2. At the very bottom of the file, just before app.Run();, add these routes:
// --- AUTHENTICATION ENDPOINTS (Outside WebSocket) ---

app.MapPost("/api/auth/login", async (
    [FromServices] SignInManager<IdentityUser> signInManager,
    [FromForm] string email, 
    [FromForm] string password) =>
{
    var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: false, lockoutOnFailure: false);
    
    if (result.Succeeded) return Results.Redirect("/dashboard");
    
    return Results.Redirect("/login?error=Invalid+credentials");
}).DisableAntiforgery(); 

app.MapPost("/api/auth/logout", async ([FromServices] SignInManager<IdentityUser> signInManager) =>
{
    await signInManager.SignOutAsync();
    return Results.Redirect("/");
}).DisableAntiforgery();

app.Run(); // This line must always be the last in the file!

Activity 4: Login and Logout Interfaces (25 min)

  1. The Login Page (Components/Pages/Login.razor):
    Create this file. It is a standard HTML form that targets our API. The data-enhance="false" attribute prevents Blazor from intercepting the click via WebSocket.
@page "/login"

<div class="row justify-content-center mt-5">
    <div class="col-md-4 card shadow-sm p-4">
        <h3 class="text-center mb-4">Login</h3>
        
        @if (!string.IsNullOrEmpty(Error))
        {
            <div class="alert alert-danger">@Error</div>
        }

        <form method="post" action="/api/auth/login" data-enhance="false">
            <div class="mb-3">
                <label>Email</label>
                <input type="text" name="email" class="form-control" required />
            </div>
            <div class="mb-3">
                <label>Password</label>
                <input type="password" name="password" class="form-control" required />
            </div>
            <button type="submit" class="btn btn-primary w-100">Log in</button>
        </form>
    </div>
</div>

@code {
    [SupplyParameterFromQuery]
    public string? Error { get; set; }
}

2. The Top Bar (Components/Layout/MainLayout.razor):
We are going to replace the static "About" link at the top right with our dynamic authentication block.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <!-- We replace the content of the top-row -->
        <div class="top-row px-4">
            <AuthorizeView Context="authState">
                <Authorized>
                    <span class="me-3">👤 @authState.User.Identity?.Name</span>
                    <!-- Mandatory POST form for HTTP logout -->
                    <form method="post" action="/api/auth/logout" data-enhance="false" class="d-inline">
                        <button type="submit" class="btn btn-sm btn-outline-primary">Logout</button>
                    </form>
                </Authorized>
                <NotAuthorized>
                    <a href="/login" class="btn btn-primary btn-sm">Log in</a>
                </NotAuthorized>
            </AuthorizeView>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

Activity 5: Securing the Application (Authorization) (20 min)

Now, we apply our business rules: only the Admin can add or edit a sensor.

  1. Protect the backend (The page):
    Open Components/Pages/EditSensor.razor. At the very top, under @page, add the attribute requiring the Admin role. If a visitor types the URL manually, they will be blocked.
@attribute [Authorize(Roles = "Admin")]

2. Protect the frontend (Hide buttons):
Open Components/Pages/MyDashboard.razor. Surround the "New Sensor" button with the verification tag:

<AuthorizeView Roles="Admin">
    <Authorized>
        <a href="/edit-sensor" class="btn btn-success mb-3">+ New Sensor</a>
    </Authorized>
</AuthorizeView>

3. Global Test:


🚀 Practice Exercises (On your own)

Exercise 1: Secure the data table

In the Components/SensorTable.razor component, the "Edit" and "Delete" buttons for each row are still visible to everyone (even if they throw an error on click if attempted).
Mission: Surround these buttons (inside the @foreach loop, within the <td> tag) with <AuthorizeView Roles="Admin"> to hide them visually from mere visitors.

Exercise 2: Confidential KPI (Granular Authorization)

In the Data field, some financial or critical indicators must only be seen by management.
Mission: In MyDashboard.razor, the Radial Gauge displaying the "Max Value Detected" is considered confidential.
Use <AuthorizeView Roles="Admin"> around the <div class="col-md-4"> that contains this gauge so the interface adapts dynamically: visitors will only see the bar chart, while the Admin will see the chart AND the gauge.


🏁 LAB 11 Summary