Refresh Tokens în .NET
Implementare corectă (nu copy-paste)
Seria: Security by Design în .NET: De la JWT la Certificate-based Authentication
În articolul anterior am explicat cum funcționează JSON Web Tokens (JWT) și cum validează ASP.NET Core semnătura.
Problema este că JWT-urile sunt stateless.
Odată emis un token:
-
serverul nu îl mai poate revoca ușor
-
tokenul rămâne valid până la expirare
-
dacă este furat → atacatorul îl poate folosi
De aceea sistemele moderne folosesc Refresh Tokens.
🔐 Ce este un Refresh Token
Un Refresh Token este un token pe termen lung, folosit pentru a obține un nou Access Token fără ca utilizatorul să se logheze din nou.
Fluxul tipic:
1️⃣ User se autentifică
2️⃣ Server emite:
-
Access Token (ex: 10 minute)
-
Refresh Token (ex: 7 zile)
3️⃣ Când Access Token expiră:
Clientul trimite:
<code>POST /auth/refresh</code>
cu Refresh Token.
Serverul verifică tokenul și emite un nou Access Token.
🔁 Token Rotation (practica modernă)

Un refresh token nu trebuie reutilizat.
De fiecare dată când este folosit:
1️⃣ Serverul invalidează tokenul vechi
2️⃣ Creează un refresh token nou
3️⃣ Returnează Access Token + Refresh Token nou
Flux:
RT1 -> folosit -> invalidat
↓
RT2 -> folosit -> invalidat
↓
RT3
Beneficiu major: Dacă un atacator fură RT1, dar userul folosește deja RT2 → sistemul detectează atacul.
🧱 Schema corectă de bază de date
Un Refresh Token trebuie stocat în DB. Exemplu simplificat:
CREATE TABLE RefreshTokens
(
Id UNIQUEIDENTIFIER PRIMARY KEY,
UserId UNIQUEIDENTIFIER NOT NULL,
TokenHash NVARCHAR(200) NOT NULL,
CreatedAt DATETIME2 NOT NULL,
ExpiresAt DATETIME2 NOT NULL,
RevokedAt DATETIME2 NULL,
ReplacedByTokenId UNIQUEIDENTIFIER NULL,
CreatedByIp NVARCHAR(50)
);
Important:
✔ tokenul este hash-uit
✔ nu stochezi tokenul raw
🔒 Model C# pentru Refresh Token
public class RefreshToken
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string TokenHash { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime? RevokedAt { get; set; }
public Guid? ReplacedByTokenId { get; set; }
public string CreatedByIp { get; set; }
public bool IsActive =>
RevokedAt == null && DateTime.UtcNow < ExpiresAt;
}
🧠 Endpoint Refresh în ASP.NET Core
Exemplu simplificat:
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(RefreshRequest request)
{
var token = await _repo.GetByTokenHash(request.RefreshToken);
if (token == null || !token.IsActive)
return Unauthorized();
var newAccessToken = _jwtService.GenerateAccessToken(token.UserId);
var newRefreshToken = _tokenService.CreateRefreshToken(token.UserId);
token.RevokedAt = DateTime.UtcNow;
token.ReplacedByTokenId = newRefreshToken.Id;
await _repo.SaveChanges();
return Ok(new
{
accessToken = newAccessToken,
refreshToken = newRefreshToken.Token
});
}
🚨 Detectarea Reuse Attack
Un reuse attack apare când un refresh token deja invalidat este folosit din nou.
Scenariu:
1️⃣ User primește RT1
2️⃣ Atacatorul fură RT1
3️⃣ User folosește RT1 → primește RT2
4️⃣ Atacatorul încearcă RT1
Serverul detectează:
<code>RT1.RevokedAt != null</code>
Acțiune recomandată:
-
revoci toate tokenurile utilizatorului
-
forțezi reautentificarea
⏳ Sliding vs Absolute Expiration
Există două strategii.
Sliding Expiration
Tokenul se prelungește la fiecare refresh.
Exemplu:
Refresh token = 7 zile
User face refresh zilnic
→ tokenul continuă să fie valid
Avantaj:
✔ experiență bună pentru user
Dezavantaj: ⚠ sesiuni foarte lungi
Absolute Expiration
Tokenul expiră definitiv după o perioadă fixă. Exemplu:
Max lifetime = 30 zile
Chiar dacă faci refresh: → după 30 zile trebuie login.
🛡️ Best Practices Refresh Tokens
✔ Hash token în DB
✔ Token rotation obligatoriu
✔ Revocation list
✔ Detect reuse attack
✔ IP logging
✔ Device tracking
✔ Expirare limitată
✔ Token random lung (256 bits)
🔐 Unde se stochează Refresh Token pe client?
Recomandat:
HttpOnly Secure Cookie
Nu în:
❌ LocalStorage
❌ SessionStorage
Motiv: XSS poate fura tokenul.
🧱 Arhitectură modernă
Client (SPA / Blazor)
Access Token -> memorie
Refresh Token -> HttpOnly cookie
Server:
JWT stateless
Refresh Tokens stateful (DB)
Acesta este modelul folosit de majoritatea sistemelor enterprise.
🎯 Concluzie
JWT singur nu este suficient. Refresh Tokens rezolvă:
✔ expirarea scurtă a tokenurilor
✔ sesiuni persistente
✔ revocarea accesului
✔ detectarea atacurilor
Dar doar dacă sunt implementate corect.