Sur un projet .NET / React, la question revient toujours :
JWT, cookies, OAuth2, IdentityServer, Auth0… on prend quoi ?
L’objectif n’est pas d’utiliser la techno la plus « hype », mais de choisir un schéma d’authentification adapté à la surface d’attaque, au contexte (intranet, B2C, B2B, mobile…) et à l’équipe qui maintient. Voici ma grille de décision, basée sur des cas réels, avec exemples concrets côté .NET et React.
1) JWT « classique » (SPA React + API .NET)
C’est le schéma que j’utilise le plus souvent pour une SPA moderne : React en front, API .NET indépendante en backend.
✅ Quand je l’utilise
- SPA React qui appelle une API .NET via HTTP (CORS, domaines séparés).
- Besoin d’un backend stateless (containers, scaling horizontal).
- Authentification via formulaire maison ou via un provider OAuth2 (Google, GitHub…) mais traduction en JWT interne pour l’API.
🔹 Avantages
- Simple à mettre en place sur un projet from scratch.
- Très scalable (pas de session serveur à partager).
- Compatible avec plusieurs clients : SPA, mobile, autre front, CLI…
⚠️ Points d’attention
- Stockage du token :
- éviter
localStoragepour les access tokens, - préférer un cookie HttpOnly (
Secure+SameSite=Lax/Strict).
- éviter
- Rotation / invalidation :
- access token courte durée (15–30 min),
- refresh token distinct, stocké en cookie HttpOnly,
- possibilité de versionner les tokens côté base pour les invalider après changement de mot de passe.
🧩 Exemple .NET – configuration JWT dans Program.cs
// Program.cs
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://auth.farzadbagheri.fr",
ValidAudience = "fb-api",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)
)
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
🧩 Exemple .NET – endpoint de login qui renvoie un JWT + cookie HttpOnly
// AuthController.cs (exemple simplifié)
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
private readonly IUserService _users;
public AuthController(IConfiguration config, IUserService users)
{
_config = config;
_users = users;
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest req)
{
var user = await _users.ValidateCredentialsAsync(req.Email, req.Password);
if (user is null) return Unauthorized();
var accessToken = CreateJwt(user);
var refreshToken = Guid.NewGuid().ToString("N");
// TODO: persister le refreshToken en base (UserTokens…)
Response.Cookies.Append(".fb.r", refreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddDays(7)
});
return Ok(new
{
accessToken,
user = new { user.Id, user.Email, user.DisplayName }
});
}
private string CreateJwt(AppUser user)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)
);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim("name", user.DisplayName),
new Claim("role", user.Role) // Admin, User, etc.
};
var token = new JwtSecurityToken(
issuer: "https://auth.farzadbagheri.fr",
audience: "fb-api",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(20),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
🧩 Exemple React – login + appel d’API avec fetch
Le front ne manipule que l’access token en mémoire, les refresh sont gérés par cookie HttpOnly.
// authApi.ts
let accessToken: string | null = null;
export async function login(email: string, password: string) {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // important pour le cookie HttpOnly
body: JSON.stringify({ email, password })
});
if (!res.ok) throw new Error("Bad credentials");
const data = await res.json();
accessToken = data.accessToken;
return data.user;
}
export async function apiGet(url: string) {
const res = await fetch(url, {
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined,
credentials: "include"
});
if (res.status === 401) {
// éventuellement appeler /api/auth/refresh ici…
}
if (!res.ok) throw new Error("API error");
return res.json();
}
2) Cookies + session (auth server-side)
Ici, on utilise l’auth classique ASP.NET Core + cookies. Le serveur garde l’état de session, le front est majoritairement rendu côté serveur (Razor, MVC, Blazor Server, etc.).
✅ Quand c’est adapté
- Application full server-side (Razor pages, MVC).
- Intranet / back-office interne avec utilisateurs connus.
- Peu ou pas de SPA dédiée (un peu de JS, mais pas une grosse app React).
🔹 Avantages
- Implémentation native dans ASP.NET Core.
- Protection CSRF bien gérée si on suit les bons patterns (anti-forgery token, mêmes domaines, etc.).
- Gestion de droits directement côté serveur (filters, policies…).
🔹 Limites
- Moins pratique si plusieurs clients doivent consommer l’API (mobile, SPA, partenaires).
- La session doit être gérée si on scale (sticky sessions ou session distribuée).
🧩 Exemple .NET – cookies d’authentification
// Program.cs (ASP.NET Core MVC ou Razor)
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.Cookie.Name = ".fb.auth";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
🧩 Exemple .NET – login MVC
// AccountController.cs (extrait)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid) return View(model);
var user = await _userService.ValidateAsync(model.Email, model.Password);
if (user is null)
{
ModelState.AddModelError("", "Identifiants invalides");
return View(model);
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.DisplayName),
new Claim(ClaimTypes.Role, user.Role)
};
var identity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity));
return RedirectToAction("Index", "Home");
}
🧩 Exemple Razor – page protégée
@* HomeController.cs *@
[Authorize]
public IActionResult Index()
{
return View();
}
@* Views/Home/Index.cshtml *@
@{
ViewData["Title"] = "Dashboard";
}
<h2>Bonjour @User.Identity!.Name</h2>
<p>Tu es connecté avec un cookie d'authentification serveur.</p>
3) OAuth2 / OpenID Connect (Google, Azure AD, Auth0…)
Ici, on délègue l’authentification à un provider externe (Google, Azure AD / Entra ID, Auth0, IdentityServer, Keycloak…).
✅ Mon choix par défaut si…
- On veut du SSO (une seule identité pour plusieurs applications).
- On veut éviter de gérer les mots de passe nous-mêmes.
- Contexte entreprise (Azure AD, Entra ID, Okta… déjà en place).
Le pattern que j’utilise souvent :
- L’utilisateur se connecte via le provider (ex : Azure AD).
- Le backend récupère le profil, crée/associe un utilisateur interne.
- Le backend génère un JWT interne pour l’API + gère les rôles côté base.
🧩 Exemple .NET – OpenID Connect avec Azure AD
// Program.cs (schéma cookie + OIDC Azure AD)
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = ".fb.oidc";
options.Cookie.HttpOnly = true;
})
.AddOpenIdConnect(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.ClientId = builder.Configuration["AzureAd:ClientId"]!;
options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"]!;
options.ResponseType = "code";
options.SaveTokens = true; // garde access/refresh en AuthenticationProperties
options.Scope.Add("email");
options.Scope.Add("profile");
options.CallbackPath = "/signin-oidc";
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
🧩 Exemple – transformer un login OIDC en JWT interne pour l’API
Après le login OIDC, on peut émettre un JWT interne qui servira pour l’API consommée par une SPA ou une appli mobile.
// ApiTokenController.cs (exemple très simplifié)
[Authorize]
[ApiController]
[Route("api/internal-token")]
public class ApiTokenController : ControllerBase
{
private readonly IConfiguration _config;
private readonly IUserSyncService _sync;
public ApiTokenController(IConfiguration config, IUserSyncService sync)
{
_config = config;
_sync = sync;
}
[HttpPost]
public async Task<IActionResult> CreateApiToken()
{
// Claims venant d'Azure AD / OIDC
var externalId = User.FindFirst("oid")?.Value ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
if (externalId is null || email is null)
return BadRequest("Profil OIDC incomplet");
// Synchroniser / créer l'utilisateur interne + rôles
var user = await _sync.EnsureUserAsync(externalId, email, User);
var accessToken = CreateInternalJwt(user);
return Ok(new { accessToken });
}
private string CreateInternalJwt(AppUser user)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)
);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim("role", user.Role),
new Claim("tenant", user.TenantId.ToString())
};
var token = new JwtSecurityToken(
issuer: "https://auth.farzadbagheri.fr",
audience: "fb-api",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
4) Recommandation rapide par type de projet
🟢 Petit projet SPA public (.NET + React)
- Formulaire de login classique.
- JWT court (15–30 min) + refresh token stocké en cookie HttpOnly.
- Rôles simples (User / Admin) gérés dans la base.
🟣 Application métier d’entreprise
- OpenID Connect (Azure AD / Entra ID, IdP d’entreprise).
- SSO entre plusieurs applications internes.
- Possibilité d’émettre un JWT interne pour des APIs techniques.
🔵 Site vitrine / admin classique
- Rendu serveur (Razor / MVC / Blazor Server).
- Cookies + auth server-side (schéma cookie natif ASP.NET Core).
- Protection CSRF, anti-forgery token sur les formulaires sensibles.
Conclusion
Il n’y a pas une seule solution magique. Ce qui compte, c’est d’aligner l’authentification sur :
- la surface d’attaque réelle (public, interne, B2B, mobile…),
- les compétences de l’équipe (ops, dev, sécurité),
- les contraintes d’infra (cloud, on-prem, multi-apps).
L’important n’est pas de « faire fancy », mais de choisir un schéma d’authentification que ton équipe comprend, surveille et maintient facilement.