Suite à mon article sur le choix entre JWT, cookies et OAuth2 sur un projet .NET / React, voici une version orientée schémas d’architecture : qui parle à la fois au dev, à l’architecte et à l’ops.
Pour chaque cas, tu as :
- un diagramme ASCII simple,
- le flux d’authentification,
- un extrait de code .NET,
- et un exemple React quand c’est pertinent.
1) Architecture SPA React + API .NET avec JWT + refresh en cookie HttpOnly
Cas typique : une SPA React qui discute avec une API .NET hébergée séparément, le tout derrière un reverse proxy (NGINX, Azure App Gateway, etc.).
+------------------+ HTTPS +--------------------+ +-------------+
| Navigateur | --------------> | API .NET (REST) | -----> | SQL / DB |
| React SPA | <-------------- | + Auth / JWT | <----- | (utilisateurs,
| | JSON + JWT | | | tokens...) |
+------------------+ +--------------------+ +-------------+
^ ^
| |
| cookie HttpOnly ".fb.r" (refresh) |
+-----------------------------------------+
/auth/refresh
Idée : l’access token vit en mémoire (ou dans un state manager), le refresh token est dans un cookie HttpOnly. L’API est complètement stateless.
Flux simplifié
- L’utilisateur soumet son email + mot de passe à
/api/auth/login. - L’API valide, génère un access token (JWT court) + un refresh token.
- Le refresh est envoyé dans un cookie HttpOnly, l’access token dans la réponse JSON.
- La SPA stocke l’access token en mémoire et le met en en-tête
Authorization: Bearer. - Quand l’access expire, la SPA appelle
/api/auth/refresh(cookie envoyé automatiquement).
.NET – configuration minimale JWT + CORS pour SPA
// Program.cs (exemple condensé JWT + CORS SPA)
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(options =>
{
options.AddPolicy("spa", p =>
p.WithOrigins("http://localhost:3000", "https://farzadbagheri.fr")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});
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();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("spa");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
.NET – endpoint /auth/refresh (rotation simple)
// AuthController.cs (extrait - refresh simple)
[HttpPost("refresh")]
public async Task<IActionResult> Refresh()
{
var refresh = Request.Cookies[".fb.r"];
if (string.IsNullOrEmpty(refresh)) return Unauthorized();
var result = await _tokenService.ValidateAndRotateAsync(refresh);
if (!result.Success) return Unauthorized();
// Nouveau refresh token en HttpOnly
Response.Cookies.Append(".fb.r", result.NewRefreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddDays(7)
});
// Access token renvoyé en JSON
return Ok(new { accessToken = result.AccessToken });
}
React – hook d’auth simplifié
// useAuth.ts (simplifié)
import { useState, useCallback } from "react";
let accessToken: string | null = null;
export function useAuth() {
const [user, setUser] = useState<{ email: string } | null>(null);
const login = useCallback(async (email: string, password: string) => {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ email, password })
});
if (!res.ok) throw new Error("Bad credentials");
const data = await res.json();
accessToken = data.accessToken;
setUser({ email: data.user.email });
}, []);
const apiGet = useCallback(async (url: string) => {
const res = await fetch(url, {
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined,
credentials: "include"
});
if (res.status === 401) {
const r = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include"
});
if (r.ok) {
const { accessToken: newToken } = await r.json();
accessToken = newToken;
const retry = await fetch(url, {
headers: { Authorization: `Bearer ${newToken}` },
credentials: "include"
});
if (!retry.ok) throw new Error("API error");
return retry.json();
}
}
if (!res.ok) throw new Error("API error");
return res.json();
}, []);
return { user, login, apiGet };
}
2) Architecture MVC / Razor avec cookies + auth server-side
Architecture classique pour un intranet ou un back-office : tout est rendu côté serveur, quelques scripts JS pour enrichir l’UI, mais pas de grosse SPA.
+------------------+ HTTPS +--------------------------+ +-------------+
| Navigateur | <----------------->| ASP.NET Core MVC / | ----->| SQL / LDAP |
| (HTML + Razor) | HTML + cookies | Razor (Auth + Views) | <----- | (utilisateurs)
+------------------+ +--------------------------+ +-------------+
^ |
| cookie ".fb.auth" (ClaimsPrincipal) |
+-----------------------------------------+
Le cookie contient un ClaimsPrincipal signé par le serveur.
La logique d’autorisation se fait côté serveur via les attributs
[Authorize], les policies, etc.
.NET – configuration cookies
// Program.cs (MVC + Cookies)
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".fb.auth";
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/Denied";
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
Contrôleur de login + page protégée
// AccountController.cs (extrait)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid) return View(model);
var user = await _users.ValidateAsync(model.Email, model.Password);
if (user is null)
{
ModelState.AddModelError("", "Identifiants incorrects");
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");
}
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard()
{
return View();
}
Ici, pas besoin de gérer des tokens côté front : le navigateur envoie
automatiquement le cookie sur chaque requête, et ASP.NET reconstruit le
User côté serveur.
3) Architecture entreprise : IdP (Azure AD) + API .NET + React
Dans beaucoup de contextes entreprise, l’authentification est centralisée dans un Identity Provider (Azure AD / Entra ID, Okta, Auth0…).
+-----------------------+
| Azure AD / IdP |
| (OpenID Connect) |
+-----------+-----------+
^
(redir OIDC) | 1) Login / Consent
|
+--------------------+---------------------+
| WebApp .NET (MVC / Razor / minimal) |
| + React SPA hébergée |
+--------------------+--------------------+
|
3) JWT interne / cookie
|
HTTPS v
+-----------+-----------+ +-------------+
| API .NET (REST) | -----> | SQL / DB |
| Auth = JWT interne | | (utilisateurs,
+-----------------------+ | permissions)
+-------------+
Flux simplifié
- L’utilisateur est redirigé vers Azure AD (OpenID Connect).
- Après login, Azure AD renvoie un id_token + access_token à l’app .NET.
- L’app mappe l’utilisateur vers un compte interne (création/sync si besoin).
- L’app émet un JWT interne pour l’API, ou signe un cookie.
- La SPA consomme l’API avec ce JWT interne (ou via un BFF suivant la stratégie).
.NET – configuration OpenID Connect + Cookie
// Program.cs (WebApp avec OIDC Azure AD + cookie)
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.SlidingExpiration = 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 les tokens d'Azure AD
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();
.NET – endpoint pour émettre le JWT interne consommé par React
// ApiTokenController.cs (WebApp)
[Authorize]
[ApiController]
[Route("api/internal-token")]
public class ApiTokenController : ControllerBase
{
private readonly IUserSyncService _sync;
private readonly IConfiguration _config;
public ApiTokenController(IUserSyncService sync, IConfiguration config)
{
_sync = sync;
_config = config;
}
[HttpPost]
public async Task<IActionResult> Create()
{
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");
var user = await _sync.EnsureUserAsync(externalId, email, User);
var token = CreateInternalJwt(user);
return Ok(new { accessToken = token });
}
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 jwt = new JwtSecurityToken(
issuer: "https://auth.farzadbagheri.fr",
audience: "fb-api",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
}
React – récupération du JWT interne après login OIDC
Ici, la partie « login Azure AD » est gérée par le backend (redirections). La SPA récupère simplement le token interne après coup.
// useInternalToken.ts
import { useEffect, useState } from "react";
export function useInternalToken() {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
(async () => {
const res = await fetch("/api/internal-token", {
method: "POST",
credentials: "include"
});
if (res.ok) {
const data = await res.json();
setToken(data.accessToken);
}
})();
}, []);
return token;
}
Résumé
- SPA publique → JWT + refresh en cookie HttpOnly, API stateless.
- Intranet / back-office → cookies + auth server-side (MVC, Razor).
- App entreprise avec SSO → OpenID Connect (Azure AD) + JWT interne.
À chaque fois, le schéma d’auth doit rester lisible sur un tableau blanc. Si tu n’arrives pas à le dessiner clairement, c’est probablement trop compliqué pour l’équipe qui devra le maintenir en production.