From 5c80102dcacdf08a06186c2129c9fb9301104d46 Mon Sep 17 00:00:00 2001 From: Nikolaz Date: Tue, 15 Sep 2026 20:13:23 +0200 Subject: [PATCH] Auth : rejeter les jetons dont le compte n'existe plus (validation de session) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrige un contournement : après réinitialisation de la base (même JWT_SECRET_KEY), un jeton restait cryptographiquement valide et donnait accès alors que le compte n'existait plus, permettant d'atteindre l'onboarding sans s'inscrire. - Backend : JwtBearer.OnTokenValidated vérifie désormais l'existence du compte référencé par le claim sub (lu sous 'sub' ou NameIdentifier selon le remapping) et rejette le jeton (401) si le compte est introuvable. - Backend : nouvel endpoint protégé GET /api/auth/moi (SessionController) renvoyant 200 si la session est réellement valide, 401 sinon. - Frontend : authGuard valide la session côté serveur (GET /auth/moi) au lieu de se fier au seul jeton local ; sur échec, redirection vers /login (jeton purgé par l'intercepteur sur 401). AuthService.validerSession() ajouté. Tests backend : 54/54 au vert. --- .../Controllers/SessionController.cs | 26 ++++++++++++++++ backend/SchotzIadKmTracker.Api/Program.cs | 31 +++++++++++++++++++ frontend/src/app/core/guards/auth.guard.ts | 29 ++++++++++++----- .../src/app/core/services/auth.service.ts | 9 ++++++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 backend/SchotzIadKmTracker.Api/Controllers/SessionController.cs 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();