diff --git a/backend/SchotzIadKmTracker.Api/Controllers/SessionController.cs b/backend/SchotzIadKmTracker.Api/Controllers/SessionController.cs
new file mode 100644
index 0000000..35c8289
--- /dev/null
+++ b/backend/SchotzIadKmTracker.Api/Controllers/SessionController.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace SchotzIadKmTracker.Api.Controllers;
+
+///
+/// Endpoint de validation de session. Ce controller est soumis à l'autorisation globale
+/// (RequireAuthenticatedUser) : il n'est atteignable qu'avec un jeton valide ET un compte
+/// existant (le middleware JWT rejette un jeton dont le compte est introuvable via
+/// OnTokenValidated). Le frontend l'appelle pour confirmer qu'une session est réellement
+/// valide au chargement des routes protégées.
+///
+[ApiController]
+[Route("api/auth")]
+public class SessionController : ControllerBase
+{
+ ///
+ /// Retourne 200 si la session est valide (jeton valide + compte existant), 401 sinon.
+ ///
+ [HttpGet("moi")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public IActionResult Moi()
+ {
+ return Ok(new { authentifie = true });
+ }
+}
diff --git a/backend/SchotzIadKmTracker.Api/Program.cs b/backend/SchotzIadKmTracker.Api/Program.cs
index dc78ece..be2fbdd 100644
--- a/backend/SchotzIadKmTracker.Api/Program.cs
+++ b/backend/SchotzIadKmTracker.Api/Program.cs
@@ -137,6 +137,37 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecretKey)),
ClockSkew = TimeSpan.FromSeconds(30) // tolérance d'horloge réduite
};
+
+ // Un jeton peut être cryptographiquement valide (bonne signature, non expiré) alors
+ // que le compte n'existe plus (ex. base réinitialisée avec la même clé JWT). On vérifie
+ // donc l'EXISTENCE du compte référencé par le claim `sub` à chaque requête : sinon le
+ // jeton est rejeté (401), ce qui force la reconnexion / l'inscription côté frontend.
+ options.Events = new JwtBearerEvents
+ {
+ OnTokenValidated = async context =>
+ {
+ // ASP.NET Core remappe par défaut le claim « sub » vers ClaimTypes.NameIdentifier.
+ // On cherche donc l'identifiant sous les deux noms possibles.
+ var principal = context.Principal;
+ var sub = principal?.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value
+ ?? principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
+
+ if (!int.TryParse(sub, out var compteId))
+ {
+ context.Fail("Jeton invalide : identifiant de compte manquant.");
+ return;
+ }
+
+ var db = context.HttpContext.RequestServices.GetRequiredService();
+ var compteExiste = await db.Comptes.AsNoTracking()
+ .AnyAsync(c => c.Id == compteId, context.HttpContext.RequestAborted);
+
+ if (!compteExiste)
+ {
+ context.Fail("Compte introuvable : jeton non valide.");
+ }
+ }
+ };
});
builder.Services.AddScoped {
+export const authGuard: CanActivateFn = (): Observable => {
const authService = inject(AuthService);
const router = inject(Router);
- return authService.estAuthentifie() ? true : router.createUrlTree(['/login']);
+ // Pas de jeton local : inutile d'interroger le serveur, on redirige directement.
+ if (!authService.estAuthentifie()) {
+ return of(router.createUrlTree(['/login']));
+ }
+
+ // Jeton présent : on confirme sa validité réelle auprès du serveur.
+ return authService.validerSession().pipe(
+ map(() => true as boolean | UrlTree),
+ catchError(() => of(router.createUrlTree(['/login'])))
+ );
};
diff --git a/frontend/src/app/core/services/auth.service.ts b/frontend/src/app/core/services/auth.service.ts
index a1b9880..7f2cf72 100644
--- a/frontend/src/app/core/services/auth.service.ts
+++ b/frontend/src/app/core/services/auth.service.ts
@@ -55,6 +55,15 @@ export class AuthService {
return this.http.get<{ existe: boolean }>(`${this.baseUrl}/compte-existe`);
}
+ /**
+ * Valide la session côté serveur (GET /auth/moi, endpoint protégé).
+ * 200 => le jeton est valide ET le compte existe ; toute erreur (401, etc.)
+ * signifie que la session n'est pas réellement valide.
+ */
+ validerSession(): Observable<{ authentifie: boolean }> {
+ return this.http.get<{ authentifie: boolean }>(`${this.baseUrl}/moi`);
+ }
+
/** Retourne le token courant, ou null si non authentifié. */
getToken(): string | null {
return this._token();