Refresh Tokens în .NET
Doru Bulubasa
04 March 2026

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.