Refresh Token — access + refresh juftligi, rotation strategy

~25 daq · .NET Backend · Autentifikatsiya va Avtorizatsiya

Muammodan boshlaylik

Access token 15 daqiqa yashaydi (59-dars) — o'g'irlansa, zarar cheklangan. Lekin foydalanuvchi mobil ilovada har 15 daqiqada parol kiritishi kerakmi? Hech kim bunday ilovani ishlatmaydi.

Tokenni 30 kunlik qilsak — o'g'irlangan token 30 kun ishlaydi, va uni bekor qilib bo'lmaydi (JWT stateless).

Ikkalasini qanday birlashtirish mumkin?


Ikki token

Access token Refresh token
Muddat Qisqa — 5–15 daqiqa Uzoq — 7–30 kun
Qayerga yuboriladi Har bir API so'roviga Faqat /auth/refresh ga
Format JWT (stateless) Tasodifiy satr, bazada saqlanadi
Bekor qilish Qiyin Oson — bazadan o'chirish
1. Login        ──▶ access (15 daq) + refresh (30 kun)
2. API so'rovlar ──▶ Authorization: Bearer <access>
3. Access eskirdi ──▶ 401
4. POST /auth/refresh { refresh } ──▶ yangi access + yangi refresh
5. Chiqish (logout) ──▶ refresh bazadan bekor qilinadi

Hayotiy o'xshatma

Sport zali. Kunlik bilet (access token) — kirishda tekshiriladi, ertaga yaroqsiz. Yillik abonement (refresh token) — seyfda saqlanadi, faqat kassada ko'rsatiladi: har kuni kassada abonementni ko'rsatib yangi kunlik bilet olasiz. Abonement yo'qolsa — kassaga qo'ng'iroq qilib uni bekor qilasiz, va o'g'ri endi bilet ololmaydi.


Bazadagi model

public class RefreshToken
{
    public int Id { get; set; }
    public string TokenXesh { get; set; } = "";      // tokenning o'zi emas — xeshi
    public int FoydalanuvchiId { get; set; }
    public Foydalanuvchi Foydalanuvchi { get; set; } = null!;
    public DateTime Yaratilgan { get; set; } = DateTime.UtcNow;
    public DateTime Tugaydi { get; set; }
    public DateTime? BekorQilingan { get; set; }
    public string? AlmashtirilganXesh { get; set; }  // rotatsiyada keyingi token

    public bool Faol => BekorQilingan is null && DateTime.UtcNow < Tugaydi;
}
mb.Entity<RefreshToken>().HasIndex(r => r.TokenXesh).IsUnique();

[!INFO] Refresh token — bazada xesh ko'rinishida saqlanadi (parol kabi). Baza sizib chiqsa, xeshlardan haqiqiy tokenlarni olib bo'lmaydi. Token yuqori entropiyali tasodifiy satr bo'lgani uchun bcrypt shart emas — SHA256 yetarli.


Yaratish

using System.Security.Cryptography;

public static class RefreshYordamchi
{
    public static string Yarat() =>
        Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));

    public static string Xesh(string token) =>
        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
}
public record TokenJuftligi(string AccessToken, string RefreshToken);

private async Task<TokenJuftligi> JuftlikBerAsync(Foydalanuvchi f, CancellationToken ct)
{
    var refresh = RefreshYordamchi.Yarat();
    db.RefreshTokenlar.Add(new RefreshToken
    {
        TokenXesh = RefreshYordamchi.Xesh(refresh),
        FoydalanuvchiId = f.Id,
        Tugaydi = DateTime.UtcNow.AddDays(30)
    });
    await db.SaveChangesAsync(ct);

    return new TokenJuftligi(tokens.AccessTokenYarat(f), refresh);
}

Login endi JuftlikBerAsync(f, ct) ni qaytaradi.


Yangilash — rotatsiya bilan

public record RefreshDto(string RefreshToken);

[HttpPost("refresh")]
[AllowAnonymous]
public async Task<ActionResult<TokenJuftligi>> Refresh(RefreshDto dto, CancellationToken ct)
{
    var xesh = RefreshYordamchi.Xesh(dto.RefreshToken);
    var eski = await db.RefreshTokenlar
        .Include(r => r.Foydalanuvchi)
        .FirstOrDefaultAsync(r => r.TokenXesh == xesh, ct);

    if (eski is null) return Unauthorized();

    if (eski.BekorQilingan is not null)
    {
        // allaqachon ishlatilgan token qayta keldi — o'g'irlik belgisi
        await db.RefreshTokenlar
            .Where(r => r.FoydalanuvchiId == eski.FoydalanuvchiId && r.BekorQilingan == null)
            .ExecuteUpdateAsync(s => s.SetProperty(r => r.BekorQilingan, DateTime.UtcNow), ct);
        return Unauthorized();
    }

    if (!eski.Faol) return Unauthorized();

    var yangi = await JuftlikBerAsync(eski.Foydalanuvchi, ct);
    eski.BekorQilingan = DateTime.UtcNow;
    eski.AlmashtirilganXesh = RefreshYordamchi.Xesh(yangi.RefreshToken);
    await db.SaveChangesAsync(ct);

    return yangi;
}

Rotatsiya — har bir refresh da yangi refresh token beriladi, eskisi bekor qilinadi. Agar eski (ishlatilgan) token yana kelsa — kimdir uni nusxalagan: foydalanuvchining barcha tokenlari bekor qilinadi, qayta login talab etiladi.


Chiqish

[HttpPost("logout")]
[AllowAnonymous]
public async Task<IActionResult> Logout(RefreshDto dto, CancellationToken ct)
{
    var xesh = RefreshYordamchi.Xesh(dto.RefreshToken);
    await db.RefreshTokenlar
        .Where(r => r.TokenXesh == xesh && r.BekorQilingan == null)
        .ExecuteUpdateAsync(s => s.SetProperty(r => r.BekorQilingan, DateTime.UtcNow), ct);
    return NoContent();
}

[HttpPost("logout-hammasi")]
[Authorize]
public async Task<IActionResult> HammasidanChiqish(CancellationToken ct)
{
    var id = User.FoydalanuvchiId();
    await db.RefreshTokenlar
        .Where(r => r.FoydalanuvchiId == id && r.BekorQilingan == null)
        .ExecuteUpdateAsync(s => s.SetProperty(r => r.BekorQilingan, DateTime.UtcNow), ct);
    return NoContent();
}

Parol o'zgarganda ham — barcha refresh tokenlarni bekor qiling.

[!WARNING] Logout dan keyin access token muddati tugaguncha (≤15 daqiqa) ishlashda davom etadi — JWT ni bekor qilib bo'lmaydi. Shuning uchun access token muddati qisqa bo'lishi muhim. Darhol bloklash kerak bo'lsa (masalan, hisob buzilgan) — jti qora ro'yxati (Redis da) yoki foydalanuvchida "token versiyasi" claim i.


Mijozda saqlash

  • Brauzer — refresh token httpOnly; Secure; SameSite=Strict cookie da, Path=/api/auth: JavaScript o'qiy olmaydi, faqat refresh endpoint iga yuboriladi
  • Mobil — Keychain (iOS) / Keystore (Android)
Response.Cookies.Append("refresh", juftlik.RefreshToken, new CookieOptions
{
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.Strict,
    Path = "/api/auth",
    Expires = DateTimeOffset.UtcNow.AddDays(30)
});

Tez-tez uchraydigan xatolar

Refresh token — JWT, bazada yo'q — bekor qilib bo'lmaydi; asosiy foyda yo'qoladi.

Tokenni ochiq saqlash — baza sizsa, hamma sessiyalar o'g'irlanadi; xesh saqlang.

Rotatsiyasiz — o'g'irlangan refresh token 30 kun sezilmay ishlaydi.

Eski tokenlarni tozalamaslik — jadval cheksiz o'sadi; muddati o'tganlarni fon vazifasi bilan o'chiring.


Xulosa

Ikki token: qisqa muddatli access (JWT, har so'rovda) va uzoq muddatli refresh (tasodifiy satr, bazada xesh holida, faqat /auth/refresh ga). Access eskirsa — refresh orqali yangi juftlik.

Rotatsiya: har refresh da yangi token, eskisi bekor; ishlatilgan token qayta kelsa — barcha sessiyalar bekor. Logout — refresh ni bekor qilish (access qisqa muddat yashashda davom etadi). Saqlash — httpOnly cookie yoki mobil xavfsiz omborda.


Keyingi dars

63-dars: Role-based Authorization — Admin, User, Manager rollari