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 :
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.).
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
Nous allons configurer le moteur d'Identity, propager l'état de sécurité dans Blazor, et créer un Administrateur par défaut.
_Imports.razor) :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");
}
}
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).
Program.cs, ajoutez using Microsoft.AspNetCore.Mvc; tout en haut.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 !
Components/Pages/Login.razor) :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>
Maintenant, nous appliquons nos règles métiers : seul l'Admin peut ajouter ou modifier un capteur.
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 :
admin@data.com / Admin123!.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.
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.
Login) et la déconnexion (Logout) doivent passer par des Minimal APIs (MapPost) en HTTP standard via des formulaires HTML (action="...").<CascadingAuthenticationState> : Transmet l'état de l'utilisateur à travers tout l'arbre de composants de l'application.<AuthorizeView> : Le composant UI qui affiche ou masque du HTML selon l'état de connexion (<Authorized>, <NotAuthorized>) ou selon un Rôle défini (Roles="Admin").[Authorize] : L'attribut Backend qui empêche l'accès direct à une Page (URL) ou un Endpoint API si l'utilisateur ne possède pas les droits requis.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:
ASP.NET Core Identity has its own SQL tables ready to use (to manage hashed passwords, roles, etc.).
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
We will configure the Identity engine, propagate the security state in Blazor, and create a default Administrator.
_Imports.razor):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");
}
}
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).
Program.cs, add using Microsoft.AspNetCore.Mvc; at the very top.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!
Components/Pages/Login.razor):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>
Now, we apply our business rules: only the Admin can add or edit a sensor.
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:
admin@data.com / Admin123!.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.
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.
Login) and disconnecting (Logout) must pass through Minimal APIs (MapPost) in standard HTTP via HTML forms (action="...").<CascadingAuthenticationState>: Transmits the user state throughout the application's component tree.<AuthorizeView>: The UI component that displays or hides HTML according to the connection state (<Authorized>, <NotAuthorized>) or according to a defined Role (Roles="Admin").[Authorize]: The Backend attribute that prevents direct access to a Page (URL) or an API Endpoint if the user does not possess the required rights.