IlmHamroh
JavaScript Full-stack/30-qism. NestJS56/65-dars22 daqiqa
Mundarija (30)

Access/refresh token va cookie NestJS'da: rotation, chiqish va 15 daqiqa muammosi

Qisqacha: Nest'da access token odatda Authorization sarlavhasida, refresh token esa HttpOnly cookie'da yuradi. Ikkalasini ikki Passport strategiyasi tekshiradi: JwtStrategy va cookie'dan o'qiydigan JwtRefreshStrategy. Har yangilashda refresh almashtiriladi (rotation), eski refresh qayta kelsa — butun "oila" bekor qilinadi. Lekin bitta muammo qoladi: chiqishdan keyin ham access token muddati tugaguncha ishlaydi. Biz o'lchadik — 200 qaytdi. Shuning uchun vazifalar-api 28-qismdagi server sessiyasida qoladi.

Bu darsda

  • JwtRefreshStrategy ni cookie'dan token o'qiydigan qilib yozasiz va cookie-parser nega kerakligini bilasiz.
  • Refresh rotation va reuse detection'ni Nest xizmatida amalga oshirasiz.
  • Cookie sozlamalarini (httpOnly, secure, sameSite, path) Nest controller'ida to'g'ri qo'yasiz.
  • "Chiqishdan keyin access token ishlaydimi?" savoliga haqiqiy so'rov bilan javob berasiz va yechimlarini taqqoslaysiz.
  • vazifalar-api da sessiya cookie'sini Nest'ga ulaysiz va Nest'ga xos birinchi xatoni — idempotentlik kalitida saqlanib qolgan 401 ni — tuzatasiz.

Oldin bilishingiz kerak: Passport NestJS'da, Access va refresh token strategiyasi, Tokenni qayerda saqlash va CSRF, Session vs JWT qarori, Middleware Nest'da.

O'tgan darsda Passport bilan bitta access token chiqardik. 15 daqiqadan keyin u o'ladi va foydalanuvchi qayta parol kiritishi kerak. Bu darsda shu muammoni Nest'ning standart usulida hal qilamiz, keyin bu usulning narxini o'lchaymiz.

1. Nega bu kerak?

Access va refresh token strategiyasi darsida g'oyani o'rgandingiz: access — qisqa (15 daqiqa), har so'rovda yuradi; refresh — uzun (7 kun), faqat yangilash uchun. Refresh o'g'irlansa — rotation va reuse detection himoya qiladi.

Nest loyihalarida bu sxema juda keng tarqalgan. Ko'p jamoalar uni "Nest auth" deganda tushunadi: ikki strategiya, ikki sir, refresh cookie'da. Shuning uchun uni to'liq yozamiz.

Lekin 28-qismda biz boshqa yo'lni tanlagandik — server sessiyasi. Sabab bitta jumla edi: "chiqish darhol ishlasin". Bu darsda o'sha jumlani Nest'da haqiqiy so'rov bilan tekshiramiz.

O'xshatish: access token — konsert bileti. Qorovul faqat biletning soxta emasligini tekshiradi, kassaga qo'ng'iroq qilmaydi. Tez. Lekin bilet egasini zaldan haydasangiz ham, bileti kechgacha amal qiladi. Server sessiyasi esa — eshikdagi ro'yxat: har kirishda qorovul ro'yxatga qaraydi. Sekinroq, lekin ismni o'chirsangiz — darhol kirolmaydi.

2. Ikki token — ikki strategiya

Access token sarlavhada yuradi — o'tgan darsdagi JwtStrategy. Refresh esa HttpOnly cookie'da: JavaScript uni o'qiy olmaydi, shuning uchun XSS uni o'g'irlay olmaydi (tokenni qayerda saqlash). Ikkinchi strategiya kerak, u tokenni cookie'dan oladi:

ts
// refresh.strategy.ts — refresh token HttpOnly cookie'dan
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import type { Request } from "express";

export const REFRESH_SIRI =
  "boshqa-sir-refresh-uchun-32-belgidan-uzun";
export type RefreshTolov = { sub: number; jti: string; oila: string };

const cookiedan = (req: Request): string | null => {
  const c = req.cookies as Record<string, string> | undefined;
  return c?.["refresh"] ?? null;
};

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
  Strategy, "jwt-refresh") {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([cookiedan]),
      secretOrKey: REFRESH_SIRI,
      algorithms: ["HS256"],
    });
  }

  validate(p: RefreshTolov): RefreshTolov {
    return p; // bazadagi holat — TokenlarService.aylantir() da
  }
}

Uch narsaga qarang:

  • Nom — "jwt-refresh". Bitta passport-jwt dan ikki strategiya yasadik, ular nom bilan farqlanadi.
  • Sir boshqa. Access siri bilan refresh imzolansa, refresh'ni access o'rnida yuborish mumkin bo'lardi. Ikki sir — ikki alohida kalit.
  • ExtractJwt.fromExtractors([...]) — o'z funksiyangiz. U req.cookies dan o'qiydi.

req.cookies Express'da o'zidan paydo bo'lmaydi. Uni cookie-parser middleware'i yaratadi: main.ts da app.use(cookieParser()). Nest hujjati ham shu paketni tavsiya qiladi. Biz cookie-parser@1.4.7 ni o'rnatdik.

2.2 Payload ichida nima bor

Refresh payload'ida uch maydon: sub (kim), jti (shu tokenning noyob raqami) va oila (birinchi kirishdan beri bir zanjir). jti — JWT ID, standart claim (JWT anatomiyasi). U orqali server har refresh'ni alohida taniydi.

3. Rotation va reuse detection

3.1 Xizmat

Refresh tokenning holati serverda saqlanadi. Demoda — Map, haqiqiy loyihada — jadval (28-qismdagi tajribada refresh_tokenlar):

ts
@Injectable()
export class TokenlarService {
  readonly #refreshlar = new Map<string, Yozuv>(); // jti → yozuv

  constructor(private readonly jwt: JwtService) {}

  async chiqar(f: Pick<Foydalanuvchi, "id" | "rol">,
    oila: string = randomUUID()): Promise<Juftlik> {
    const jti = randomUUID();
    this.#refreshlar.set(jti,
      { sub: f.id, rol: f.rol, oila, ishlatildi: false });
    const access = await this.jwt.signAsync(
      { sub: f.id, rol: f.rol });
    const refresh = await this.jwt.signAsync(
      { sub: f.id, jti, oila },
      { secret: REFRESH_SIRI, expiresIn: "7d" });
    return { access, refresh };
  }

  async aylantir(p: RefreshTolov): Promise<Juftlik> {
    const y = this.#refreshlar.get(p.jti);
    if (y === undefined) throw new UnauthorizedException();
    if (y.ishlatildi) {
      // eski token qayta keldi — o'g'irlangan bo'lishi mumkin:
      // butun oila bekor (reuse detection)
      this.bekor(y.oila);
      throw new UnauthorizedException("refresh qayta ishlatildi");
    }
    y.ishlatildi = true;
    const rol = y.rol as Foydalanuvchi["rol"];
    return this.chiqar({ id: y.sub, rol }, y.oila);
  }

  bekor(oila: string): void {
    for (const [jti, y] of this.#refreshlar)
      if (y.oila === oila) this.#refreshlar.delete(jti);
  }
}

aylantir() — darsning yuragi. Yaroqli refresh keldi: u "ishlatildi" deb belgilanadi va yangi juftlik chiqadi (o'sha oilada). Ishlatilgan refresh yana keldi: demak uning nusxasi kimdadir. Kimniki haqiqiy — server bilmaydi, shuning uchun ikkalasini ham chiqarib yuboradi.

signAsync(payload, { secret }) — ikkinchi argument modul sukutini shu chaqiruv uchun almashtiradi. Access — modul siri bilan 15 daqiqa, refresh — REFRESH_SIRI bilan 7 kun.

Demodagi Map — faqat ko'rsatish uchun. Haqiqiy loyihada uchta farq bor. Birinchidan, holat bazada: server qayta ishga tushsa yoki ikki nusxada ishlasa, Map hammani chiqarib yuboradi yoki ikki xil javob beradi. Ikkinchidan, bazada tokenning o'zi emas, uning hash'i saqlanadi — 28-qismdagi sessiya tokenlari kabi: baza o'g'irlansa ham tokenlar ishlamaydi. Uchinchidan, eski yozuvlar vaqti-vaqti bilan tozalanadi, aks holda jadval cheksiz o'sadi. Bu uchtasini qo'shsangiz — refresh jadvali 28-qismdagi sessiyalar jadvaliga juda o'xshab qoladi. Bu tasodif emas.

ts
const COOKIE: CookieOptions = {
  httpOnly: true, // JavaScript o'qiy olmaydi (XSS)
  secure: true,
  sameSite: "strict",
  path: "/auth", // faqat /auth/* so'rovlariga boradi
  maxAge: 7 * 24 * 3600 * 1000,
};

@Controller("auth")
export class RefreshController {
  constructor(private readonly tokenlar: TokenlarService) {}

  @Post("kirish")
  @HttpCode(200)
  @UseGuards(AuthGuard("local"))
  async kirish(@Req() req: Request,
    @Res({ passthrough: true }) res: Response): Promise<object> {
    const j = await this.tokenlar.chiqar(req.user as Foydalanuvchi);
    res.cookie("refresh", j.refresh, COOKIE);
    return { access: j.access };
  }

  @Post("yangila")
  @HttpCode(200)
  @UseGuards(AuthGuard("jwt-refresh"))
  async yangila(@Req() req: Request,
    @Res({ passthrough: true }) res: Response): Promise<object> {
    const j = await this.tokenlar.aylantir(req.user as RefreshTolov);
    res.cookie("refresh", j.refresh, COOKIE);
    return { access: j.access };
  }

  @Post("chiqish")
  @HttpCode(204)
  @UseGuards(AuthGuard("jwt-refresh"))
  chiqish(@Req() req: Request,
    @Res({ passthrough: true }) res: Response): void {
    this.tokenlar.bekor((req.user as RefreshTolov).oila);
    res.clearCookie("refresh", { ...COOKIE, maxAge: undefined });
  }
}

Cookie sozlamalari — cookie chuqur darsidagi qoidalar, endi Nest'da:

  • path: "/auth" — refresh /vazifalar so'rovlariga bormaydi. Kam yuradigan token — kam o'g'irlanadi.
  • sameSite: "strict" — boshqa saytdan boshlangan so'rovga cookie qo'shilmaydi (CSRF'ga qarshi birinchi qatlam).
  • @Res({ passthrough: true }) — cookie qo'yamiz, lekin javobni Nest yuboradi. passthrough siz so'rov osilib qolardi (Controller'lar).
  • clearCookie ga o'sha path beriladi. Brauzer cookie'ni nom + domen + yo'l bo'yicha taniydi (RFC 6265). Boshqa path bilan o'chirsangiz — eski cookie qoladi.

3.3 Biz ishga tushirdik

Demo serverda sakkiz so'rov (Node 24.21.0, NestJS 12.2.0, @nestjs/jwt 12.0.3, cookie-parser 1.4.7). Tokenlarni <token> va eyJ… bilan almashtirdik, Expires sanasini olib tashladik. O'qishdan oldin uch joyni belgilab qo'ying: 3- va 4-qatordagi ikki xil 401, 6-qatordagi bo'sh refresh= va oxirgi qatordagi 200:

text
1) kirish → 200 {"access":"eyJ…"}
  Set-Cookie: refresh=<token>; Max-Age=604800; Path=/auth; HttpOnly; Secure; SameSite=Strict
2) yangila (1-cookie) → 200 {"access":"eyJ…"}
  Set-Cookie: refresh=<token>; Max-Age=604800; Path=/auth; HttpOnly; Secure; SameSite=Strict
3) yangila (1-cookie QAYTA) → 401 {"message":"refresh qayta ishlatildi","error":"Unauthorized","statusCode":401}
4) yangila (2-cookie) → 401 {"message":"Unauthorized","statusCode":401}
5) yana kirish → 200 {"access":"eyJ…"}
  Set-Cookie: refresh=<token>; Max-Age=604800; Path=/auth; HttpOnly; Secure; SameSite=Strict
6) chiqish → 204
  Set-Cookie: refresh=; Path=/auth; HttpOnly; Secure; SameSite=Strict
7) yangila (chiqqan cookie) → 401 {"message":"Unauthorized","statusCode":401}
8) chiqishdan keyin access bilan /auth/men → 200 {"user":{"id":1,"rol":"foydalanuvchi"}}

Qadamma-qadam:

  • 2 — rotation: eski cookie o'rniga yangisi keldi.
  • 3 — hujumchi o'g'irlagan eski refresh bilan keldi. Server uni tanidi va oilani o'ldirdi.
  • 4 — endi haqiqiy foydalanuvchining yangi refresh'i ham ishlamaydi. Bu ataylab: kim haqiqiy ekani noma'lum, ikkalasi chiqariladi.
  • 6–7 — chiqish refresh oilasini o'chirdi, cookie tozalandi.
  • 8 — eng muhim qator. Chiqqan foydalanuvchining access tokeni hali 200 oladi.

Tekshirib ko'ring: 4-qatorda haqiqiy foydalanuvchi ham chiqarib yuborildi. Bu xato emasmi?

Javob

Yo'q, bu reuse detection'ning maqsadi. Server ikki nusxani ko'rdi va qaysi biri hujumchiniki ekanini bila olmaydi. Hammasini bekor qilish — xavfsiz tomon: haqiqiy foydalanuvchi parol bilan qayta kiradi, hujumchi esa kira olmaydi. Narxi — bitta noqulaylik. Lekin 28-qism tajribasida yana bir narx topilgan: ikki tab bir vaqtda yangilasa, biri eski refresh'ni yuboradi va soxta hujum aniqlanadi.

4. 15 daqiqa muammosi

4.1 Nega access o'lmaydi

Access token — o'z-o'zini tasdiqlaydigan hujjat. JwtStrategy faqat imzo va exp ni tekshiradi, bazaga qaramaydi. Chiqish esa faqat bazadagi refresh'ni o'chiradi. Tokenning o'ziga hech kim "sen endi yaroqsizsan" deya olmaydi.

Paketsiz model — soxta soat bilan (brauzerda ishlaydi):

js
let hozir = 0; // soniya
const sessiyalar = new Set(["s-1"]); // server sessiyasi
const jwtlar = new Map([["jwt-1", { sub: 1, exp: 900 }]]);

const sessiyaBilan = (t) => (sessiyalar.has(t) ? 200 : 401);
const jwtBilan = (t) => {
  const p = jwtlar.get(t); // imzo to'g'ri deb olamiz
  return p !== undefined && hozir < p.exp ? 200 : 401;
};

sessiyalar.delete("s-1"); // foydalanuvchi "chiqish" ni bosdi
for (const t of [0, 60, 899, 900]) {
  hozir = t;
  console.log(`${t} s: sessiya ${sessiyaBilan("s-1")}, ` +
    `JWT ${jwtBilan("jwt-1")}`);
}

Konsolda:

text
0 s: sessiya 401, JWT 200
60 s: sessiya 401, JWT 200
899 s: sessiya 401, JWT 200
900 s: sessiya 401, JWT 401

Chiqishdan keyingi birinchi soniyadan sessiya yopiq. JWT esa 899-soniyagacha ochiq. Bloklangan hisob, o'g'irlangan telefon, "barcha qurilmalardan chiqish" — hammasi shu 15 daqiqa ichida kechikadi.

flowchart LR
  A["0 s: chiqish"] --> B["sessiya: 401 darhol"]
  A --> C["JWT: 200"]
  C --> D["900 s: exp"]
  D --> E["JWT: 401"]

Diagrammada ikki yo'l bitta nuqtadan ajraladi. Sessiyada "chiqish" va "kira olmaslik" orasida vaqt yo'q. JWT'da ular orasida butun access muddati turadi.

4.2 Kanon ham shuni o'lchadi

Kanon tajribasi 28-qism sessiyasini va JWT'ni bitta Nest ilovasida solishtirdi (kanon chiqishi):

text
### 2b. Chiqishdan keyin: sessiya bekor — keyingi so'rov 401; JWT access token muddati tugaguncha (15 daqiqa) ISHLAYDI
sessiya bekor qilingach tekshir(): undefined
JWT ("chiqish" bo'lgan foydalanuvchiniki) → 200 {"kim":1} — bekor qilish uchun denylist (bazaga murojaat) yoki qisqa muddat + refresh rotatsiyasi kerak

Nima bo'ldi? Sarlavha xulosani o'zi aytadi. Ikkinchi qatorda sessiya tekshiruvi undefined qaytardi — ya'ni "bunday sessiya yo'q", keyingi so'rov 401 oladi. Uchinchi qatorda o'sha foydalanuvchining JWT'si 200 oldi, chunki tekshiruv faqat imzo va muddatga qaradi. Izohdagi "denylist yoki qisqa muddat" — keyingi jadvaldagi yechimlar.

Bizning demo va kanon — ikki mustaqil o'lchov, natija bir xil.

4.3 Yechimlar va ularning narxi

Yechim Qanday Narxi
Qisqa access (1–5 daqiqa) oyna kichrayadi yangilash ko'payadi, oyna baribir bor
Denylist chiqqan jti lar jadvali, har so'rov tekshiradi har so'rov bazaga — JWT afzalligi yo'qoladi
Token versiyasi foydalanuvchida tokenVersiya, payload'da v har so'rov bazaga — xuddi sessiya
Server sessiyasi token — tasodifiy satr, holat bazada har so'rov bitta indeksli so'rov

Jadvalga qarang: oxirgi uch qator — aslida bir xil narsa. "Darhol bekor qilish" kerak bo'lsa, har so'rovda kimdir serverga qarashi kerak. Shunda JWT'ning "bazasiz tekshirish" afzalligi yo'qoladi, murakkabligi (alg, kalit aylantirish, oila) esa qoladi.

Token versiyasi Nest'da odatda shunday yoziladi — JwtStrategy.validate() ichida bazaga qarab:

ts
async validate(p: { sub: number; v: number }) {
  const f = await this.foydalanuvchilar.ol(p.sub); // bazaga so'rov
  if (f === null || f.tokenVersiya !== p.v) {
    throw new UnauthorizedException();
  }
  return { id: f.id, rol: f.rol };
}

"Chiqish" — tokenVersiya ni bittaga oshirish. Ishlaydi. Lekin endi har so'rov bazaga boradi — 28-qismdagi sessiya tekshiruvi bilan bir xil narx.

4.4 Nega kanonda yo'q

Kanon qarori (docs/AUTH-QAROR.md, 30-qism bo'limi) — o'lchovlar bilan:

Mezon Server sessiyasi (kanon) Access JWT + refresh
Tekshirish Postgres, ~2.3k req/s bazasiz, 3.6k req/s (1.5×)
Chiqish, bloklash darhol access 15 daqiqa ishlaydi (200)
Cookie hajmi 58 belgi access 298 belgi (har so'rovda)
Kod va xavf token, hash, muddat alg, exp, kalitlar, oila, poyga

Tezlik farqi bor, lekin 28-qismda o'lchanganidek, sessiya tekshiruvi ~1.6 ms, butun HTTP so'rov esa ~10 ms. Farq foydalanuvchiga sezilmaydi. "Chiqish darhol ishlasin", "qurilmalar ro'yxati", "hisobni bloklash" talablari esa JWT'ga to'g'ridan zid. Shuning uchun sessiya qoladi. JWT qachon qaytadi — ADR'da yozilgan: mobil ilova (cookie'siz) yoki boshqa xizmat foydalanuvchi nomidan so'rov yuborsa.

4.5 JWT qachon to'g'ri tanlov

Bu bo'limdan "JWT yomon" degan xulosa chiqmasin. U boshqa vazifa uchun yaratilgan va o'sha joyda kuchli:

  • Xizmatlar orasida. Bitta xizmat token chiqaradi, o'nta xizmat uni bazasiz, faqat ochiq kalit bilan tekshiradi (RS256 yoki EdDSA, JWKS). Markaziy sessiya bazasi kerak emas.
  • Mobil ilova. Cookie bilan ishlash noqulay, token sarlavhada tabiiy yuradi. Refresh — qurilmaning xavfsiz omborida (Keychain, Keystore).
  • Qisqa, bir martalik ruxsat. "Shu faylni 5 daqiqa yuklab olish mumkin" — imzolangan havola, bekor qilish kerak emas.

Uchala holatda ham "darhol chiqish" talabi yo'q yoki zaif. vazifalar-api da esa u asosiy talab. Tanlov texnologiyadan emas, talabdan boshlanadi — 28-qismdagi ADR shu tartibda yozilgan edi.

5. Hujumchi nigohi

  1. Refresh localStorage da. Bitta XSS — 7 kunlik kalit hujumchida. Himoya — HttpOnly cookie, path cheklangan.
  2. Refresh o'g'irlash va undan oldin yugurish. Hujumchi foydalanuvchidan oldin yangilasa, keyingi haqiqiy yangilash reuse sifatida aniqlanadi va oila o'ladi. Hujumchi ham chiqib ketadi — rotation'ning foydasi shu.
  3. CSRF orqali yangilash. Cookie avtomatik qo'shiladi. sameSite: "strict" va Origin tekshiruvi birga (CSRF darsi). vazifalar-api da bu csrfHimoya() middleware'i.
  4. Chiqqan foydalanuvchining access tokeni. Biz 8-qatorda ko'rdik — 200. Telefon o'g'irlangan bo'lsa, 15 daqiqa yetarli vaqt.
  5. Bitta sir ikki token uchun. Refresh access o'rnida ishlaydi va 7 kun yashaydi. Har token — o'z siri.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
yangila doim 401 cookie-parser ulanmagan — req.cookies yo'q app.use(cookieParser())
So'rov osiladi @Res() passthrough siz @Res({ passthrough: true })
Chiqishdan keyin cookie qoladi clearCookie boshqa path bilan o'sha path va domain
Refresh /vazifalar ga ham boradi path berilmagan (/) path: "/auth"
Refresh access o'rnida o'tadi ikkalasi bitta sir bilan alohida sirlar
Ikki tab — tasodifiy chiqib ketish reuse detection poygasi yangilashni bitta tabga (navbat), qisqa grace oynasi

7. Mashqlar

Mashqlar kurs/mashqlar/30/56-refresh/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Cookie'dan token oladigan passport-jwt funksiyasi — ExtractJwt.([...])
  • req.cookies ni yaratadigan paket —
  • Refresh faqat /auth/* ga borishi uchun cookie sozlamasi —
  • JWT ning noyob raqami (standart claim) —
  • Biz o'lchagan: chiqishdan keyin access bilan /auth/men —
Yechim
  • fromExtractors — massivdagi funksiyalarni tartib bilan sinaydi, birinchi topilgan token olinadi.
  • cookie-parser — Cookie sarlavhasini req.cookies obyektiga aylantiradi.
  • path — "/auth"; o'chirishda ham aynan shu qiymat.
  • jti — server har refresh'ni shu bilan taniydi.
  • 200 — access token exp gacha yashaydi.

2-mashq (o'rta): token versiyasi

Modelga token versiyasini qo'shing. foydalanuvchilar — Map (id → { versiya }), token payload'i { sub, v, exp }. tekshir(token) — exp o'tmagan va v foydalanuvchining joriy versiyasiga teng bo'lsa 200, aks holda 401. chiqishHammasi(id) versiyani oshiradi. Keyin bitta foydalanuvchining ikki qurilmadagi tokenini chiqishdan oldin va keyin tekshiring. Ishora: Map va ++.

Yechim
js
const hozir = 100;
const foydalanuvchilar = new Map([[1, { versiya: 3 }]]);
const telefon = { sub: 1, v: 3, exp: 900 };
const noutbuk = { sub: 1, v: 3, exp: 900 };

function tekshir(t) {
  const f = foydalanuvchilar.get(t.sub); // "bazaga so'rov"
  if (f === undefined || hozir >= t.exp) return 401;
  return t.v === f.versiya ? 200 : 401;
}
function chiqishHammasi(id) {
  foydalanuvchilar.get(id).versiya++;
}

console.log("oldin:", tekshir(telefon), tekshir(noutbuk));
chiqishHammasi(1);
console.log("keyin:", tekshir(telefon), tekshir(noutbuk));
console.log("versiya:", foydalanuvchilar.get(1).versiya);

Konsolda:

text
oldin: 200 200
keyin: 401 401
versiya: 4

"Barcha qurilmalardan chiqish" darhol ishladi. Lekin tekshir() ning birinchi qatoriga qarang: har so'rovda foydalanuvchilar.get() — haqiqiy loyihada bu bazaga so'rov. Ya'ni token versiyasi JWT'ni server sessiyasiga aylantiradi, faqat murakkabroq shaklda.

3-mashq (qiyin): reuse detection va grace oynasi

Ikki tab muammosini yumshatish uchun ba'zi tizimlar grace oynasi qo'yadi: ishlatilgan refresh 10 soniya ichida yana kelsa — reuse emas, o'sha yangi juftlik qayta beriladi. 10 soniyadan keyin kelsa — oila bekor. aylantir(jti, vaqt) ni yozing: yozuvda ishlatildi (vaqt yoki null) va keyingi (berilgan yangi jti) saqlansin. Natija — "yangi:<jti>", "takror:<jti>" yoki "oila bekor". Ishora: Map, shart operatorlari.

Yechim
js
const GRACE = 10;
const refreshlar = new Map([["r1", { oila: "A", ishlatildi: null }]]);
let son = 1;

function aylantir(jti, vaqt) {
  const y = refreshlar.get(jti);
  if (y === undefined) return "401";
  if (y.ishlatildi !== null) {
    if (vaqt - y.ishlatildi <= GRACE) return `takror:${y.keyingi}`;
    for (const [k, v] of refreshlar) {
      if (v.oila === y.oila) refreshlar.delete(k);
    }
    return "oila bekor";
  }
  const yangi = `r${++son}`;
  y.ishlatildi = vaqt;
  y.keyingi = yangi;
  refreshlar.set(yangi, { oila: y.oila, ishlatildi: null });
  return `yangi:${yangi}`;
}

console.log(aylantir("r1", 100)); // 1-tab
console.log(aylantir("r1", 103)); // 2-tab, 3 s keyin
console.log(aylantir("r2", 200)); // keyingi yangilash
console.log(aylantir("r1", 500)); // eski token — hujumchi
console.log(aylantir("r3", 501)); // oila o'lgan

Konsolda:

text
yangi:r2
takror:r2
yangi:r3
oila bekor
401

Ikkinchi tab soxta hujumga aylanmadi. Lekin grace oynasi hujumchiga ham 10 soniya beradi: shu vaqt ichida o'g'irlangan refresh bilan kelsa, u ham r2 ni oladi. Xavfsizlik va qulaylik orasidagi kelishuv — server sessiyasida bu muammo umuman yo'q.

8.1 Nima qo'shiladi

28-qismdagi sessiya mantig'i (sessiya.ts) o'tgan darsda qaytdi. Bugun uni Nest'ga ulaymiz:

Fayl Nima
src/auth/auth.service.ts yangi: sessiyaBer() va 28-qism authXizmati
src/auth/auth-yarat.ts yangi: config'dan auth bog'liqliklari
src/umumiy/idempotentlik.ts tuzatish: egasi bo'yicha kalit, 401/403/429 saqlanmaydi
src/ilova.ts CSRF, kimlik, egasiniAniqla, no-store

8.2 AuthService.sessiyaBer

28-qismda bu funksiya marshrut.ts ichida edi. Nest'da u xizmat bo'ldi — kirish, OTP, Google, Telegram, passkey yo'llari hammasi shuni chaqiradi. Kanon faylidan, telefon uchun bo'lingan (izohlar qisqartirildi, kod ma'nosi aynan):

ts
// auth.service.ts — AuthController va MfaController umumiy qismi:
// 28-qism authXizmati (biznes qoidalari) va sessiyaBer (fixation
// himoyasi, bloklangan hisob).
import { Inject, Injectable } from "@nestjs/common";
import type { Request, Response } from "express";
import { IlovaXatosi } from "../umumiy/xatolar.ts";
import { AUTH } from "../tokenlar.ts";
import { cookieNomi, cookieOl } from "./sessiya.ts";
import { authXizmati } from "./xizmat.ts";
import type { AuthBogliqliklari, Mijoz } from "./xizmat.ts";
import type { Foydalanuvchi } from "./ombor.ts";

export function mijoz(req: Request): Mijoz {
  return { ip: req.ip, userAgent: req.get("user-agent") };
}

@Injectable()
export class AuthService {
  readonly xizmat: ReturnType<typeof authXizmati>;

  constructor(@Inject(AUTH) readonly d: AuthBogliqliklari) {
    this.xizmat = authXizmati(d);
  }

  async sessiyaBer(req: Request, res: Response, f: Foydalanuvchi,
    eslabQol: boolean): Promise<void> {
    if (await this.d.ombor.bloklanganmi(f.id)) {
      await this.xizmat.hodisa({ tur: "bloklangan_kirish",
        foydalanuvchiId: f.id, ...mijoz(req) });
      throw new IlovaXatosi(403,
        "Hisob bloklangan — administrator bilan bog'laning",
        { kod: "hisob-bloklangan", sarlavha: "Hisob bloklangan" });
    }
    const s = this.d.sessiyalar;
    const eski = cookieOl(req.headers.cookie, cookieNomi(s.s));
    if (eski !== undefined) await s.tokenniBekorQil(eski);
    const { token, tugash } =
      await s.yarat(f.id, mijoz(req), eslabQol);
    s.cookieQoy(res, token, tugash, eslabQol);
  }
}

AUTH tokeni ostida — authYarat() qaytaradigan obyekt: sessiyalar, tokenlar, OTP, MFA, OIDC, API kalitlar, cheklagich, CAPTCHA. auth-yarat.ts — 28-qism server.ts dagi funksiyaning aynan o'zi, faqat alohida faylda. Uni Nest provider fabrikasi chaqiradi (Custom provider'lar) — bu ulanishni Autentifikatsiya II darsida yozamiz.

8.3 Middleware tartibi

ilova.ts da, express.json dan keyin (kanon faylidan, bo'lingan):

ts
if (a !== null) {
  app.use(csrfHimoya(originlar));
  app.use(kimlik(a.sessiyalar, a.apiKalitlar));
  app.use(["/v1/auth", "/v1/admin"],
    (_req: express.Request, res: express.Response,
      next: express.NextFunction) => {
      res.set("Cache-Control", "no-store");
      next();
    });
}
app.use(egasiniAniqla(
  a === null ? (egasiId ?? c.EGASI_ID) : undefined,
  (e) => { cls.set(CLS_EGASI, e); }));

a — AUTH provider'i: auth yoqilgan bo'lsa obyekt, DEV rejimda null. Tartib: CSRF → kim → egasi. Hammasi Express middleware, ya'ni Nest marshrutlaridan, middleware'laridan va guard'laridan oldin. no-store — auth javoblari (xato ham) brauzer yoki proxy keshiga tushmasin.

Nega CSRF birinchi? O'zgartiruvchi so'rov (POST, PATCH, DELETE) cookie bilan begona sahifadan kelishi mumkin. Uni sessiyani o'qishdan oldin rad etish arzonroq va xavfsizroq: bazaga so'rov ham ketmaydi. csrfHimoya() — 28-qismdagi o'sha funksiya: Origin ruxsat ro'yxatidagi origin'lardan biri bo'lishi shart (Tokenni qayerda saqlash va CSRF). Bu Express middleware'i Nest'da ham o'zgarishsiz ishlaydi — Nest ichida Express turgani uchun.

8.4 Topilgan xato: idempotentlik kaliti 401 ni saqlardi

Endi Nest'ga xos birinchi xato. 25-qismdagi Idempotency-Key middleware'i Nest'da VazifalarModule.configure() orqali ulangan (Middleware Nest'da). Nest'ning tartibi esa qat'iy: middleware → guard → pipe → handler.

flowchart LR
  A["Express: kimlik, egasi"] --> B["Nest middleware: idempotentlik"]
  B --> C["Guard: Kirish 401/403"]
  C --> D["Guard: Cheklov 429"]
  D --> E["Handler: 201"]

Ikki ilovadagi tartibni yonma-yon qo'yamiz (anonim POST /v1/vazifalar, Idempotency-Key bilan):

Qadam Express (28-qism) Nest (ko'chishdan keyin, tuzatishgacha)
1 kimlik, egasiniAniqla kimlik, egasiniAniqla (o'sha Express middleware)
2 kirishKerak — 401, zanjir shu yerda to'xtaydi idempotentlik middleware'i — kalitni egallaydi
3 idempotentlik — bu yerga yetib kelmaydi KirishGuard — 401
4 — idempotentlik javobni (401) saqlaydi

Kod bir xil, faqat ikki qadam joy almashdi. Nest'da bu almashuvni siz yozmaysiz — uni so'rov hayot sikli qoidasi beradi: guard har doim middleware'dan keyin.

28-qismdagi Express routerida tartib boshqacha edi: kirishKerak (401) idempotentlikdan oldin turardi. Nest'da 401 beradigan guard idempotentlikdan keyin keladi. Natija: middleware javobni ushlab, kalit ostida 24 soatga saqlaydi — u 401 bo'lsa ham.

Kanon buni shunday ta'riflaydi: anonim so'rov kalitni egallab, 401 javobi 24 soat "qayta o'ynalardi". Biz holatning ikki nusxasida — tuzatishdan oldingi va keyingi — o'zimiz tekshirdik (Vitest 5.0.5, PostgreSQL 17.6). Ikki yugurish chiqishini yonma-yon qo'ydik.

Birinchi holat — anonim so'rov ikki marta, o'sha kalit bilan:

text
oldin:  anonim 1: 401 replayed=undefined
        anonim 2: 401 replayed=true
keyin:  anonim 1: 401 replayed=undefined
        anonim 2: 401 replayed=undefined

replayed=true — ikkinchi javob handler'dan emas, saqlangan nusxadan. Bundan ham yomoni: anonim so'rovlar idempotentlik omborini cheksiz to'ldira oladi.

Ikkinchi holat — kirgan foydalanuvchi. Aziz faqat o'qish ruxsatli API kaliti bilan POST yubordi (403), keyin o'sha kalit bilan sessiyadan:

text
oldin:  1) API kalit (faqat o'qish): 403
        2) sessiya, o'sha kalit: 403 replayed=true
keyin:  1) API kalit (faqat o'qish): 403
        2) sessiya, o'sha kalit: 201 replayed=undefined

Tuzatishdan oldin Aziz 24 soat davomida shu kalit bilan vazifa yarata olmasdi — har safar saqlangan 403 qaytardi. Kanon izohiga ko'ra, xuddi shunday 429 ham saqlanardi: tezlik cheklovidan bir marta o'tib ketgan kalit kun bo'yi "Juda ko'p so'rov" deb javob berardi.

Tuzatish ikki qator (kanon faylidan, qisqartirilgan):

diff
+const VAQTINCHA = new Set([401, 403, 429]);
 …
+    // egasi noma'lum (anonim — guard keyin 401 beradi): saqlanmaydi
+    if (req.egasi === undefined) {
+      next();
+      return;
+    }
     const ichkiKalit = `${String(req.egasi)}:${kalit}`;
 …
-        res.statusCode >= 500
+        res.statusCode >= 500 || VAQTINCHA.has(res.statusCode)
           ? ombor.ochir(ichkiKalit)

Mantiq: guard javobi — handler natijasi emas. Handler ishlamagan bo'lsa, saqlaydigan narsa ham yo'q. Shu xatoni ushlaydigan test (test/nest-auth.test.ts) testlar bilan birga Autentifikatsiya II darsida qo'shiladi.

Tekshirib ko'ring: Nega bu xato 28-qismda yo'q edi, axir kod deyarli o'sha?

Javob

Kod o'sha, tartib boshqa. Express'da middleware'lar siz yozgan ketma-ketlikda ishlaydi va kirishKerak idempotentlikdan oldin edi. Nest'da tartibni freymvork belgilaydi: guard har doim middleware'dan keyin. Ko'chishda faylni o'zgartirmasdan ko'chirish yetmaydi — atrofdagi tartibni ham tekshirish kerak.

8.5 Tekshiruv: ikki kutilgan xato

ilova.ts endi auth ni AppModule.forRoot() ga beradi, lekin modul hali uni qabul qilmaydi. Shu kesimni biz kanon fayllaridan yig'ib tekshirdik:

text
src/ilova.ts(23,15): error TS2305: Module '"./vazifalar/ombor.ts"' has no exported member 'OmborFabrikasi'.
src/ilova.ts(59,91): error TS2353: Object literal may only specify known properties, and 'auth' does not exist in type 'IlovaBogliqliklari'.

Tarjimasi: TS2305 — "ombor.ts modulida OmborFabrikasi degan eksport yo'q". TS2353 — "obyekt literalida faqat ma'lum xususiyatlar bo'lishi mumkin: IlovaBogliqliklari turida auth yo'q". Birinchisi — egalik qatlami (CASL darsi), ikkinchisi — AppModule (Autentifikatsiya II).

Faqat shu ikki xato bo'lsa — hammasi to'g'ri. Hali commit qilmang.

9. Real ishda

  • Ko'p Nest loyihalarida auth = access JWT (sarlavhada) + refresh (HttpOnly cookie) + rotation. Kod review'da birinchi savollar: refresh qayerda saqlanadi, rotation bormi, reuse detection bormi, chiqish nima qiladi.
  • Mobil ilovalar — cookie qulay emas, shuning uchun JWT + refresh bu yerda tabiiy tanlov. Brauzer va bitta sayt — server sessiyasi soddaroq.
  • Intervyu: "Chiqishdan keyin JWT'ni qanday bekor qilasiz?" — eng ko'p beriladigan savollardan biri. To'g'ri javob — "imkonsiz, faqat denylist, versiya yoki qisqa muddat; hammasining narxi bor".
  • Incident: "akkaunt o'g'irlandi, bloklasak ham 15 daqiqa ishladi" — JWT tizimlarida real uchraydigan shikoyat.

Xulosa

  • Refresh — HttpOnly cookie'da, alohida sir bilan, path cheklangan; Nest'da JwtRefreshStrategy + ExtractJwt.fromExtractors + cookie-parser.
  • Rotation — har yangilashda yangi juftlik; ishlatilgan refresh qayta kelsa — oila bekor.
  • Chiqishdan keyin access token exp gacha ishlaydi — biz o'lchadik: 200. Denylist va versiya buni tuzatadi, lekin har so'rov bazaga boradi.
  • Kanon qarori: brauzer uchun server sessiyasi qoladi (darhol chiqish, qurilmalar, bloklash).
  • Nest'da guard middleware'dan keyin ishlaydi — idempotentlik 401/403/429 ni saqlardi; endi egasiz va guard javoblari saqlanmaydi.

Keyingi dars: Global auth guard, @Public va @Roles — "sukut bo'yicha yopiq" guard yozamiz va ko'chishda topilgan yana ikki xatoni ko'ramiz: versiyasiz ochilib qolgan yo'llar va yopilib qolgan /health.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Access/refresh token va cookie NestJS'da: rotation, chiqish va 15 daqiqa muammosi — IlmHamroh