IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi22/31-dars25 daqiqa
Mundarija (34)

API kalitlar va servislararo autentifikatsiya: prefiks, HMAC, ruxsatlar va imzoli so'rovlar

Qisqacha: Skript, bot yoki boshqa server cookie bilan kira olmaydi — unga API kalit beriladi. Yaxshi kalit ikki qismdan iborat: ochiq prefiks (bazadan topish va ekranda ko'rsatish uchun) va 256 bitlik sir. Bazada sirning o'zi emas, HMAC'i saqlanadi; to'liq kalit faqat yaratilganda bir marta ko'rsatiladi. Kalitning ruxsatlari (scope) tor, muddati bor, uni aylantirish mumkin. Ikki o'z serveringiz orasida esa kalit yubormasdan, umumiy sir bilan har so'rovni imzolash mumkin: metod, yo'l, vaqt, nonce va tana hash'i HMAC bilan bog'lanadi.

Bu darsda

  • API kalitning tuzilishini (prefiks + sir) va nega bazada faqat HMAC turishini tushuntira olasiz.
  • Kalitga ruxsatlar, muddat va aylantirishni qo'shasiz.
  • Servislararo HMAC imzoni (vaqt oynasi, nonce, xom tana) yozasiz va uni buzib ko'rasiz.
  • Authorization: Bearer va WWW-Authenticate sarlavhalarini to'g'ri ishlatasiz.
  • vazifalar-api ga API kalitlari va ichki imzoli yo'lni qo'shasiz.

Oldin bilishingiz kerak: crypto 1: tasodif, hash va HMAC, RBAC va resurs egaligi, Tokenni qayerda saqlash va CSRF, Kiruvchi webhook'larni qabul qilish.

Hozirgacha vazifalar-api ga faqat odam kirardi: brauzer, cookie, parol yoki passkey. Lekin Aziz vazifalarini har kuni ertalab Telegram'ga yuboradigan kichik skript yozmoqchi. Skript parolni bilishi kerakmi? Yo'q. Unga alohida, cheklangan va istalgan payt o'chiriladigan kalit kerak.

1. Nega bu kerak?

1.1 Dastur odam emas

Brauzerdagi sessiya odam uchun qurilgan: login formasi, cookie, 2FA, "eslab qol". Dastur uchun bularning hammasi noqulay va xavfli:

  • skriptga parolni yozsangiz, u parol bilan hamma narsa qila oladi — parolni o'zgartirish ham;
  • parol o'zgarsa, hamma skript to'xtaydi;
  • skript 2FA kodini kirita olmaydi;
  • qaysi so'rov odamdan, qaysi skriptdan kelganini jurnalda ajratib bo'lmaydi.

API kalit bu muammolarni hal qiladi: u parol emas, alohida ishonch ma'lumoti. Uning o'z nomi, ruxsatlari, muddati bor va uni parolga tegmasdan bekor qilish mumkin.

1.2 Ikki xil vaziyat

Vaziyat Kim Usul
foydalanuvchi nomidan dastur Azizning skripti API kalit (Bearer)
o'z serverlaringiz orasida bot → API, ish navbati → API umumiy sir bilan imzo
tashqi tizim — sizning xizmatingiz hamkor kompaniya OAuth client credentials, mTLS

Bugun birinchi ikkitasini quramiz. Uchinchisi — hujjat bo'yicha tanishuv.

2. API kalitning tuzilishi

2.1 Prefiks va sir

Kanon formati:

text
vz_k7m2x9qa_AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tk
│  │        └─ sir: 32 tasodifiy bayt, base64url (43 belgi)
│  └─ prefiks: 8 belgi, ochiq
└─ tur belgisi: "vazifalar"

Bu yerdagi sir — darsdagi namuna, haqiqiy kalit emas. Har qismning vazifasi:

  • vz_ — kalit turini ko'rsatadi. Kimdir kalitni tasodifan GitHub'ga yuklasa, maxsus skanerlar uni aynan shu shakl bo'yicha topadi. GitHub o'z tokenlarini shuning uchun ghp_ kabi prefiks bilan chiqaradi.
  • Prefiks — ochiq identifikator. Server kalitni bazadan shu bo'yicha (indeks bilan) topadi. Foydalanuvchi ro'yxatda vz_k7m2x9qa_… ni ko'radi va qaysi kalit qaysi skriptniki ekanini biladi. Loglarga ham faqat prefiks yoziladi.
  • Sir — haqiqiy maxfiy qism, 256 bit tasodif.
js
const ALIFBO = "abcdefghijklmnopqrstuvwxyz0123456789";
const b64url = (b) => btoa(String.fromCharCode(...b))
  .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");

function kalitYarat() {
  const p = crypto.getRandomValues(new Uint8Array(8));
  const prefiks = Array.from(p, (b) => ALIFBO[b % 36]).join("");
  const sir = b64url(crypto.getRandomValues(new Uint8Array(32)));
  return { prefiks, sir, kalit: `vz_${prefiks}_${sir}` };
}

const k = kalitYarat();
console.log(k.kalit.length, k.sir.length);
console.log(/^vz_[a-z0-9]{8}_[A-Za-z0-9_-]{43}$/.test(k.kalit));

const soni = new Array(36).fill(0);
for (let b = 0; b < 256; b++) soni[b % 36]++;
console.log(soni[0], soni[3], soni[4], soni[35]);

Konsolda:

text
55 43
true
8 8 7 7

Kalit har safar boshqa, shuning uchun uning uzunligi va formatini chiqardik. Oxirgi qator — modulo og'ishi (modulo bias), OTP orqali login darsidan tanish. 256 ni 36 ga bo'lganda qoldiq 4: a, b, c, d harflari 8 ta bayt qiymatidan chiqadi, qolganlari 7 tadan. Prefiks sir emas, shuning uchun bu og'ish zarar qilmaydi — kanon buni ataylab soddalashtirgan. Sirda esa bunday yo'l bilan harf tanlamang: u to'g'ridan-to'g'ri baytlardan base64url bilan olinadi.

2.2 Bazada — faqat HMAC

Parolni xavfsiz saqlash darsida parolni argon2id bilan hash qildik. API kalitda ham sirni ochiq saqlab bo'lmaydi: baza sizsa, hujumchi hamma kalit bilan kiradi. Lekin argon2 bu yerda kerak emas.

Nega? argon2 ataylab sekin, chunki parol odam o'ylagan — Toshkent2026 ni milliard urinishda topish mumkin. API kalitning siri esa 256 bit tasodif: uni taxmin qilish amalda imkonsiz, sekin hash hech narsa qo'shmaydi. Har so'rovda 30 ms argon2 esa API'ni sekinlashtiradi.

Kanon tanlovi — sirning HMAC'i, kaliti ILOVA_SIRI dan HKDF bilan olingan ("api-kalit" maqsadi). Oddiy SHA-256 dan farqi: baza sizsa ham, server kalitisiz hujumchi o'z taxminini tekshira olmaydi.

js
const enc = new TextEncoder();
const serverKaliti = await crypto.subtle.importKey(
  "raw", enc.encode("namuna-server-kaliti-faqat-dars!"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const hmac = async (sir) => new Uint8Array(
  await crypto.subtle.sign("HMAC", serverKaliti, enc.encode(sir)));

function teng(a, b) {
  if (a.length !== b.length) return false;
  let farq = 0;
  for (let i = 0; i < a.length; i++) farq |= a[i] ^ b[i];
  return farq === 0;
}

const b64url = (b) => btoa(String.fromCharCode(...b))
  .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
// namuna sir: 32 bayt (haqiqiy kalitda — getRandomValues)
const SIR = b64url(Uint8Array.from({ length: 32 }, (_, i) => i * 7));
console.log(SIR);
const baza = new Map([
  ["k7m2x9qa", { egasi: 1, h: await hmac(SIR) }],
]);

async function tekshir(kalit) {
  const m = /^vz_([a-z0-9]{8})_([A-Za-z0-9_-]{43})$/.exec(kalit);
  if (m === null) return "format";
  const q = baza.get(m[1]);
  if (q === undefined) return "prefiks yo'q";
  const mos = teng(q.h, await hmac(m[2]));
  return mos ? `egasi ${q.egasi}` : "sir xato";
}

console.log(baza.get("k7m2x9qa").h.length);
console.log(await tekshir(`vz_k7m2x9qa_${SIR}`));
console.log(await tekshir(`vz_k7m2x9qa_${SIR.slice(0, -1)}A`));
console.log(await tekshir(`vz_aaaaaaaa_${SIR}`));
console.log(await tekshir("Bearer boshqa-token"));

Konsolda:

text
AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tk
32
egasi 1
sir xato
prefiks yo'q
format

Bazada 32 bayt turadi. Tekshiruv uch bosqichli: format (regex), prefiks (bazadan bitta qator), sir (HMAC va vaqtga chidamli solishtirish). Mijozga esa uchala holatda ham bir xil javob boradi — 401. Sabab tashqariga aytilmaydi.

teng — brauzerda timingSafeEqual yo'q, shuning uchun XOR bilan o'zimiz yozdik (Parolni xavfsiz saqlash darsidagi PBKDF2 mashqidek). Kanonda — Node'ning timingSafeEqual.

Tekshirib ko'ring: Nega bazada faqat HMAC bo'lsa ham, prefiks ochiq saqlanadi? Uni ham HMAC qilsa bo'lmaydimi?

Javob

Prefiks bo'yicha server kalitni topadi. Agar prefiks ham yashirilsa, server har so'rovda hamma kalitlarning HMAC'ini tekshirib chiqishi kerak bo'lardi. Prefiks ochiq bo'lgani bilan hech narsa yo'qotilmaydi: u faqat "qaysi kalit", sirsiz u hech narsa bermaydi. Shu bilan birga foydalanuvchi ro'yxatda kalitlarini prefiks bo'yicha taniydi.

2.3 Jadval va cheklovlar

Kanon migratsiyasi 121-api-kalitlar.sql ning asosiy qismi:

sql
CREATE TABLE api_kalitlar (
  id               bigint      GENERATED ALWAYS AS IDENTITY,
  foydalanuvchi_id bigint      NOT NULL,
  prefiks          text        NOT NULL,
  kalit_hmac       bytea       NOT NULL,
  nom              text        NOT NULL,
  ruxsatlar        text[]      NOT NULL,
  yaratilgan_vaqt  timestamptz NOT NULL DEFAULT now(),
  -- NULL — muddatsiz (tavsiya etilmaydi)
  tugash_vaqti     timestamptz,
  oxirgi_ishlatish timestamptz,
  bekor_vaqti      timestamptz,
  CONSTRAINT api_kalitlar_pkey PRIMARY KEY (id),
  CONSTRAINT api_kalitlar_foydalanuvchi_id_fkey
    FOREIGN KEY (foydalanuvchi_id)
    REFERENCES foydalanuvchilar (id) ON DELETE CASCADE,
  CONSTRAINT api_kalitlar_prefiks_key UNIQUE (prefiks),
  CONSTRAINT api_kalitlar_prefiks_check
    CHECK (prefiks ~ '^[a-z0-9]{8}$'),
  CONSTRAINT api_kalitlar_kalit_hmac_check
    CHECK (octet_length(kalit_hmac) = 32),
  CONSTRAINT api_kalitlar_nom_check
    CHECK (char_length(nom) BETWEEN 1 AND 60),
  CONSTRAINT api_kalitlar_ruxsatlar_check
    CHECK (cardinality(ruxsatlar) > 0 AND ruxsatlar
      <@ ARRAY['vazifalar:oqish', 'vazifalar:yozish'])
);

<@ — "chapdagi massiv o'ngdagisining ichida" (Massivlar, range, domain va maxsus turlar). Kod xato bilan noma'lum ruxsat yozmoqchi bo'lsa, baza rad etadi. Quyidagi so'rovlar kanon sxemasi (121 bilan) to'ldirilgan lokal bazada bajarildi (biz ishga tushirdik, PostgreSQL 17.6). Avval to'g'ri kalit:

sql
INSERT INTO api_kalitlar
  (foydalanuvchi_id, prefiks, kalit_hmac, nom, ruxsatlar)
VALUES (1, 'k7m2x9qa', sha256('namuna'::bytea), 'Skript',
        '{vazifalar:oqish}')
RETURNING id, 'vz_' || prefiks || '_…' AS korinish, ruxsatlar,
  octet_length(kalit_hmac) AS hmac_bayt;

Natija:

text
 id |   korinish    |     ruxsatlar     | hmac_bayt
----+---------------+-------------------+-----------
  1 | vz_k7m2x9qa_… | {vazifalar:oqish} |        32
(1 row)

INSERT 0 1

sha256(...) — faqat 32 baytlik namuna qiymat; haqiqiy HMAC'ni ilova hisoblaydi. Ro'yxat yo'li aynan shunday korinish qaytaradi — sirsiz. Endi noma'lum ruxsat:

sql
\set VERBOSITY terse
INSERT INTO api_kalitlar
  (foydalanuvchi_id, prefiks, kalit_hmac, nom, ruxsatlar)
VALUES (1, 'b4n8w2ze', sha256('x'::bytea), 'Admin',
        '{vazifalar:oqish, admin:hammasi}');

Natija:

text
ERROR:  new row for relation "api_kalitlar" violates check constraint "api_kalitlar_ruxsatlar_check"

Va oldingi darsdagi qoida — ma'lumot roli kalitlarni ko'rmaydi:

sql
SET ROLE vazifalar_api;
SELECT count(*) FROM api_kalitlar;

Natija:

text
SET
ERROR:  permission denied for table api_kalitlar

Migratsiyada buning uchun alohida REVOKE ALL ON api_kalitlar FROM vazifalar_ilova bor. Oldingi darsdagi topilmani eslang: ALTER DEFAULT PRIVILEGES har yangi jadvalni ma'lumot roliga ochib qo'yadi. Yangi auth jadvali — yangi REVOKE.

3. Ruxsatlar, muddat va aylantirish

3.1 Ruxsatlar (scope)

Aziz skripti faqat vazifalarni o'qiydi. Unga yozish huquqini berish — eng kam huquq tamoyiliga zid. Kanonda ikki ruxsat: vazifalar:oqish va vazifalar:yozish. Qaysi biri kerakligi metoddan aniqlanadi: GET/HEAD — o'qish, qolgani — yozish. Yetmasa — 403 ruxsat-yoq.

Bundan ham muhimi — kalit nima qila olmasligi:

  • yangi kalit yarata olmaydi va boshqa kalitni bekor qila olmaydi;
  • /men, admin yo'llari, parol va 2FA boshqaruvi — kalit bilan 401.

Aks holda bitta sizgan "o'qish" kaliti o'ziga "yozish" kalitini yasab olardi. Kalitlarni faqat sessiya bilan (ya'ni odam, parol va 2FA bilan) boshqarish mumkin.

3.2 Muddat va oxirgi ishlatish

tugash_vaqti — kalit o'zi o'ladigan payt (1–365 kun). Muddatsiz kalit unutiladi va yillar davomida yashaydi. oxirgi_ishlatish — foydalanuvchi "bu kalit hali kerakmi?" degan savolga javob topsin. Har so'rovda bazaga yozmaslik uchun kanon uni faqat 60 soniyadan eski bo'lsa yangilaydi.

3.3 Aylantirish

Kalit sizdi yoki shunchaki almashtirish vaqti keldi. Eskisini darhol o'chirsangiz, ishlab turgan skript to'xtaydi. Kanon yechimi — aylantirish (rotation):

  1. yangi kalit yaratiladi (o'sha nom va ruxsatlar bilan);
  2. eskisining tugash_vaqti 24 soatdan keyinga qo'yiladi;
  3. 24 soat davomida ikkalasi ham ishlaydi — siz skriptni yangilaysiz.

Kalit sizgan bo'lsa esa aylantirish emas, bekor qilish (DELETE) — darhol.

3.4 Bearer va CSRF

Kalit Authorization sarlavhasida keladi:

text
GET /v1/vazifalar HTTP/1.1
Authorization: Bearer vz_k7m2x9qa_AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tk

Yaroqsiz kalitga javob — 401 va RFC 6750 dagi sarlavha:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="vazifalar", error="invalid_token"

Cookie bilan farqi muhim. Noto'g'ri cookie jim o'tkaziladi (so'rov anonim deb davom etadi, himoyalangan yo'lda 401). Noto'g'ri Bearer esa darhol 401: dastur kalit eskirganini bilishi kerak, aks holda u xatoni boshqa joydan qidiradi.

Kalitni URL'ga (?api_key=...) qo'ymang: URL server loglariga, brauzer tarixiga va proksi jurnallariga tushadi. Sarlavha — to'g'ri joy.

Va bir qiziq natija: Bearer so'rovida CSRF tekshiruvi kerak emas. Tokenni qayerda saqlash va CSRF darsini eslang. CSRF brauzer cookie'ni o'zi qo'shgani uchun ishlaydi. Authorization sarlavhasini brauzer hech qachon o'zi qo'shmaydi — uni faqat kalitni biladigan kod yozadi. Kanon testi buni tekshiradi: yozish kaliti begona Origin bilan ham 201.

4. Servislararo autentifikatsiya

4.1 Kalit yubormasdan isbotlash

Endi 29-qismdagi Telegram botingiz vazifalar-api ga so'rov yubormoqchi. Ikkalasi ham sizniki. Bot kimning nomidan emas — u xizmat. Unga foydalanuvchi API kaliti mos emas.

Oddiy yechim — umumiy maxfiy satrni sarlavhada yuborish. Kamchiligi: so'rov biror joyda (log, proksi, xato hisoboti) yozilib qolsa, sir ham ketadi. Va bir marta tutib olingan so'rovni istalgancha qayta yuborish mumkin.

Yaxshiroq yechim — imzo. Sir hech qachon yuborilmaydi, u bilan har so'rovning HMAC'i hisoblanadi. Kiruvchi webhook'larni qabul qilish darsida to'lov tizimining imzosini tekshirgansiz — bu o'sha g'oya, faqat endi ikkala tomon ham siz.

4.2 Nima imzolanadi

Kanon imzolanadigan satri — beshta qator:

text
POST
/v1/ichki/tekshiruv
1800000000
n0nce-namuna-12345
<tana SHA-256 hex>

Har qator bitta hujumni yopadi:

Qator Bo'lmasa
metod GET imzosi bilan DELETE
yo'l (so'rov parametrlari bilan) boshqa yo'lga yuborish
vaqt (unix soniya) eski so'rovni bir haftadan keyin qayta yuborish
nonce 5 daqiqa ichida qayta yuborish
tana hash'i tanani almashtirish

Sarlavhalar: X-Vaqt, X-Nonce, X-Imzo (64 belgili hex). Server o'zi xuddi shu satrni yig'adi, HMAC hisoblaydi va solishtiradi.

js
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
  (x) => x.toString(16).padStart(2, "0")).join("");
const sha256 = async (t) =>
  hex(await crypto.subtle.digest("SHA-256", enc.encode(t)));

const ICHKI_SIR = await crypto.subtle.importKey(
  "raw", enc.encode("namuna-ichki-sir-faqat-dars-uchun"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);

async function imzola(metod, yol, vaqt, nonce, tana) {
  const satr = [metod, yol, vaqt, nonce, await sha256(tana)]
    .join("\n");
  const mac = await crypto.subtle.sign(
    "HMAC", ICHKI_SIR, enc.encode(satr));
  return hex(mac);
}

const YOL = "/v1/ichki/tekshiruv";
const VAQT = "1800000000";
const NONCE = "n0nce-namuna-12345";
const a = await imzola("POST", YOL, VAQT, NONCE, '{"vazifa":1}');
const b = await imzola("POST", YOL, VAQT, NONCE, '{"vazifa": 1}');
const c = await imzola("POST", YOL, "1800000001", NONCE,
  '{"vazifa":1}');
console.log(a.length, a.slice(0, 16));
console.log(b.slice(0, 16), a === b);
console.log(c.slice(0, 16), a === c);
console.log((await sha256('{"vazifa":1}')).slice(0, 16));

Konsolda:

text
64 475324dfe41c1810
5431c6b4325f3ac6 false
dff3d74659dc471f false
d137d96c5ab5d2e7

Ikkinchi qatorga qarang: tanaga bitta bo'shliq qo'shildi ("vazifa": 1) — imzo butunlay boshqa. Uchinchisi — vaqt bir soniyaga farq qiladi, imzo yana boshqa. Faqat boshini ko'rsatdik — 64 belgi telefonga sig'maydi.

4.3 Xom tana tuzog'i

Bo'shliq misoli serverda jiddiy tuzoqqa olib keladi. Express express.json() tanani o'qib, req.body ni obyekt qiladi. Server imzoni JSON.stringify(req.body) dan hisoblasa-chi? Mijoz {"vazifa": 1} (bo'shliq bilan) yubordi, server esa {"vazifa":1} ni hisobladi — imzo hech qachon mos kelmaydi. Yoki teskarisi: ikki xil tana bir xil obyektga aylanib, "tana o'zgarmadi" deb noto'g'ri xulosa chiqadi.

To'g'ri yo'l — mijoz yuborgan aynan baytlar ustida hisoblash. Express'da buning uchun verify funksiyasi bor, u tanani parse qilishdan oldin xom Buffer ni beradi:

ts
// verify: xom baytlar — servislararo HMAC imzosi aynan yuborilgan
// baytlar ustida (JSON qayta seriyalanganda bo'shliqlar o'zgaradi)
app.use(express.json({
  limit: "16kb",
  type: JSON_TURLARI,
  verify: (req, _res, buf) => {
    (req as express.Request).xomTana = buf;
  },
}));

Webhook darsida ham aynan shu saboqni ko'rgansiz: imzo — baytlar ustida, obyekt ustida emas.

4.4 Vaqt oynasi va nonce

Imzo to'g'ri bo'lsa ham, so'rov tutib olinib, qayta yuborilgan bo'lishi mumkin — bu takroriy hujum (replay attack). Ikki himoya:

  1. Vaqt oynasi: X-Vaqt server soatidan ±300 soniyadan ko'p farq qilsa — rad. Eski so'rovni ertaga yuborib bo'lmaydi.
  2. Nonce: oyna ichida ham har nonce bir marta. Server ko'rilgan nonce'larni oyna davomida eslab qoladi.
js
function tekshiruvchi(oynaS = 300) {
  const korilgan = new Map();
  return (vaqt, nonce, hozir) => {
    if (Math.abs(hozir - vaqt) > oynaS) return "vaqt";
    for (const [n, t] of korilgan) if (t < hozir) korilgan.delete(n);
    if (korilgan.has(nonce)) return "takror";
    korilgan.set(nonce, hozir + 2 * oynaS);
    return "ok";
  };
}

const t = tekshiruvchi();
const H = 1_800_000_000;
console.log(t(H, "a1", H));
console.log(t(H, "a1", H + 10));
console.log(t(H - 360, "b2", H));
console.log(t(H + 100, "c3", H));
console.log(t(H + 700, "a1", H + 700));

Konsolda:

text
ok
takror
vaqt
ok
ok

Beshinchi qator — eski nonce a1 700 soniyadan keyin yana qabul qilindi. Bu xato emas: nonce xotiradan tozalandi, lekin so'rovning o'z vaqti (H + 700) yangi. Haqiqiy hujumchi esa eski so'rovni eski X-Vaqt bilan qayta yuboradi — u birinchi tekshiruvda "vaqt" bilan rad bo'ladi. Ikki himoya bir-birini to'ldiradi: nonce'ni abadiy saqlash shart emas, faqat oyna davomida.

Nonce xotirada — bitta jarayon uchun. Serveringiz bir nechta nusxada ishlasa, nonce bir nusxada ko'rilgan, boshqasida — yo'q. U holda umumiy ombor kerak: Redis'dagi SET nonce 1 NX EX 600 (Distributed lock va rate limit Redis'da) — "faqat yo'q bo'lsa yoz" bitta atomik buyruq.

sequenceDiagram
  participant B as Bot
  participant A as vazifalar-api
  Note over B: satr = METOD, YO'L, VAQT, NONCE, sha256(tana)
  B->>A: POST /v1/ichki/tekshiruv + X-Vaqt, X-Nonce, X-Imzo
  A->>A: sarlavhalar formati
  A->>A: vaqt ±300 s
  A->>A: HMAC(xom tana) va timingSafeEqual
  A->>A: nonce oyna ichida yangi
  A-->>B: 200 yoki 401 imzo-xato

4.5 Boshqa yo'llar: client credentials va mTLS

Hamkor kompaniyaning serveri sizning API'ngizga ulanmoqchi bo'lsa, ko'pincha boshqa standartlar ishlatiladi. Ikkalasi ham kursda sinalmagan (hujjat bo'yicha):

  • OAuth 2.0 client credentials (RFC 6749, "Client Credentials Grant" bo'limi) — servis client_id va client_secret bilan avtorizatsiya serveridan qisqa muddatli access token oladi va API'ga shu bilan murojaat qiladi. Sir faqat token olishda ishlatiladi, har so'rovda emas. OAuth 2.0 va OpenID Connect nazariyasi darsidagi oqimlarning "foydalanuvchisiz" turi.
  • mTLS (o'zaro TLS) — har ikki tomon sertifikat ko'rsatadi; server mijozni sertifikati bo'yicha taniydi. Ichki tarmoqlarda va bank integratsiyalarida keng tarqalgan. Sertifikatlarni chiqarish va yangilash — alohida infratuzilma.

5. Hujumchi nigohi

Har qatorda: hujum, himoya va uni qotiradigan test (api-kalit.test.ts).

Hujum Himoya Test
bazadan kalitlarni o'g'irlash faqat HMAC, server kaliti .env da "bazada sir yo'q (HMAC)"
GitHub'ga yuklangan kalit vz_ prefiksi, skanerlash, bekor qilish bekor qilingan — 401
"o'qish" kaliti bilan yozish metod → ruxsat oqish: POST — 403
kalit bilan yangi kalit yasash faqat sessiya kalit bilan kalitlar, /men — 401
kalit URL'da faqat Authorization — (qoida)
imzoli so'rovni qayta yuborish vaqt oynasi + nonce 6 daqiqa eski, takror nonce — 401
tanani almashtirish sha256(xom tana) satrda o'zgartirilgan tana — 401
begona kalitni bekor qilish (IDOR) WHERE foydalanuvchi_id begona kalit — 404

6. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
kalitni bazada ochiq saqlash sizsa — hammasi ochiq HMAC (sir 256 bit)
API kalitni argon2 bilan har so'rovga ~30 ms HMAC yetarli
kalitni qayta ko'rsatish "keyin ko'raman" — ko'chirib qolish faqat yaratilganda bir marta
muddatsiz kalit unutiladi, yillar yashaydi 1–365 kun
hamma ruxsat bitta kalitda sizsa — hamma narsa tor ruxsatlar
imzoni JSON.stringify(req.body) dan bo'shliq farqi xom baytlar (verify)
nonce faqat xotirada, ko'p nusxa nusxalar bir-birini bilmaydi Redis SET NX
aylantirishda eskisini darhol o'chirish ishlab turgan skript to'xtaydi 24 soatlik oraliq

7. Mashqlar

Mashqlar kurs/mashqlar/28/22-api-kalit/ da.

1-mashq (oson): to'g'ri yoki noto'g'ri

  1. API kalitning siri parol kabi argon2id bilan hash qilinishi kerak.
  2. Prefiks sir emas, uni loglarga yozish mumkin.
  3. Bearer kalit bilan keladigan so'rovda CSRF tekshiruvi shart.
  4. Servislararo imzoda tana hash'i satrga kirmasa, tanani almashtirish mumkin.
  5. Nonce'ni abadiy saqlash shart, aks holda eski so'rov qayta o'tadi.
Yechim
  1. Noto'g'ri — 256 bitlik tasodifiy sirni taxmin qilib bo'lmaydi; HMAC yetarli va tez.
  2. To'g'ri — u faqat "qaysi kalit"; sirsiz hech narsa bermaydi.
  3. Noto'g'ri — brauzer Authorization ni o'zi qo'shmaydi, CSRF cookie bilan bog'liq.
  4. To'g'ri — imzo faqat metod, yo'l, vaqt va nonce'ni himoya qilardi.
  5. Noto'g'ri — vaqt oynasi eski so'rovni rad etadi; nonce faqat oyna davomida kerak.

2-mashq (o'rta): imzolovchi mijoz

Bot tomonini yozing: imzoliSarlavhalar(metod, yol, tana, vaqt, nonce) funksiyasi { "X-Vaqt", "X-Nonce", "X-Imzo" } obyektini qaytarsin (imzo — darsdagi satr formatida, HMAC-SHA256, hex). So'ng server tomonidagi imzoTogrimi(metod, yol, sarlavhalar, tana) funksiyasi bilan uch holatni tekshiring: o'sha tana, tanaga bo'shliq qo'shilgan, yo'l ?limit=1 bilan o'zgartirilgan. Sir — "namuna-ichki-sir-faqat-dars-uchun".

Yechim
js
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
  (x) => x.toString(16).padStart(2, "0")).join("");
const sha256 = async (t) =>
  hex(await crypto.subtle.digest("SHA-256", enc.encode(t)));
const kalit = await crypto.subtle.importKey(
  "raw", enc.encode("namuna-ichki-sir-faqat-dars-uchun"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);

async function imzo(metod, yol, vaqt, nonce, tana) {
  const satr = [metod, yol, vaqt, nonce, await sha256(tana)]
    .join("\n");
  const mac = await crypto.subtle.sign(
    "HMAC", kalit, enc.encode(satr));
  return hex(mac);
}

async function imzoliSarlavhalar(metod, yol, tana, vaqt, nonce) {
  return {
    "X-Vaqt": vaqt,
    "X-Nonce": nonce,
    "X-Imzo": await imzo(metod, yol, vaqt, nonce, tana),
  };
}

async function imzoTogrimi(metod, yol, s, tana) {
  const kutilgan =
    await imzo(metod, yol, s["X-Vaqt"], s["X-Nonce"], tana);
  return kutilgan === s["X-Imzo"];
}

const YOL = "/v1/ichki/tekshiruv";
const tana = '{"vazifa":1}';
const s = await imzoliSarlavhalar("POST", YOL, tana,
  "1800000000", "mashq-nonce-000001");
console.log(Object.keys(s).join(", "), s["X-Imzo"].length);
console.log(await imzoTogrimi("POST", YOL, s, tana));
console.log(await imzoTogrimi("POST", YOL, s, '{"vazifa": 1}'));
console.log(await imzoTogrimi("POST", `${YOL}?limit=1`, s, tana));

Konsolda:

text
X-Vaqt, X-Nonce, X-Imzo 64
true
false
false

Uchinchi holat — yo'lga so'rov parametri qo'shildi va imzo buzildi. Kanon yo'lni req.originalUrl dan oladi — u parametrlarni ham o'z ichiga oladi. Brauzer misolida qisqalik uchun ===; serverda hex'ni Buffer ga aylantirib timingSafeEqual bilan solishtiring.

3-mashq (qiyin): ruxsat va muddat

Kalit obyekti: { ruxsatlar: [...], tugash: <unix ms yoki null>, bekor: <boolean> }. kalitQarori(kalit, metod, hozir) funksiyasini yozing: bekor yoki muddati o'tgan — 401; metod uchun kerakli ruxsat (GET/HEAD → vazifalar:oqish, qolgani → vazifalar:yozish) yo'q — 403; aks holda 200. Beshta holatni sinang (hozir = 1_800_000_000_000).

Yechim
js
const OQISH = new Set(["GET", "HEAD"]);

function kalitQarori(kalit, metod, hozir) {
  if (kalit.bekor) return 401;
  if (kalit.tugash !== null && kalit.tugash <= hozir) return 401;
  const kerak = OQISH.has(metod)
    ? "vazifalar:oqish"
    : "vazifalar:yozish";
  return kalit.ruxsatlar.includes(kerak) ? 200 : 403;
}

const HOZIR = 1_800_000_000_000;
const KUN = 86_400_000;
const oqish = {
  ruxsatlar: ["vazifalar:oqish"], tugash: HOZIR + KUN, bekor: false,
};

console.log(kalitQarori(oqish, "GET", HOZIR));
console.log(kalitQarori(oqish, "POST", HOZIR));
console.log(kalitQarori(oqish, "GET", HOZIR + 2 * KUN));
console.log(kalitQarori({ ...oqish, bekor: true }, "GET", HOZIR));
console.log(kalitQarori({ ...oqish, tugash: null }, "HEAD", HOZIR));

Konsolda:

text
200
403
401
401
200

Tartib muhim: avval "kalit tirikmi" (401), keyin "ruxsat bormi" (403) — oldingi darsdagi 401/403 qoidasi. Oxirgi holat — muddatsiz kalit ishlaydi, lekin kanon formasi muddatni 1–365 kun oralig'ida so'raydi; null faqat muddat ko'rsatilmaganda.

8. Loyiha qadami: API kalitlari va ichki imzo

8.1 Migratsiya va sozlamalar

sql/121-api-kalitlar.sql (= prisma/migrations/20261028120000_api_kalitlar; schema.prisma ga ApiKalit modeli) — yuqoridagi jadval va huquqlar: GRANT ... TO vazifalar_auth, REVOKE ALL ... FROM vazifalar_ilova. HKDF maqsadi api-kalit qo'shiladi. Servislararo sir — ICHKI_HMAC_SIRI (ixtiyoriy; berilmasa ichki yo'l yo'q). Uni .env ga yozasiz, kodga emas.

8.2 src/auth/api-kalit.ts

Modulning ikki qismi. ApiKalitlar sinfining asosiy usullari: telefon uchun bo'lingan, qisqalik uchun usullarning qaytish turi yozilmagan (TypeScript uni o'zi chiqaradi). Shu ko'rinishdagi modul tip, lint va testlardan o'tdi — biz tekshirdik. royxat va bekor — oddiy SELECT/UPDATE ... WHERE foydalanuvchi_id:

ts
const KALIT = /^vz_([a-z0-9]{8})_([A-Za-z0-9_-]{43})$/;
const ALIFBO = "abcdefghijklmnopqrstuvwxyz0123456789";

/** @returns to'liq kalit (BIR MARTA ko'rsatiladi) */
async yarat(fid: number, nom: string,
  ruxsatlar: readonly Ruxsat[], muddatKun?: number) {
  // 36^8 ≈ 2.8·10^12 — to'qnashsa UNIQUE xatosi (amalda yo'q)
  const prefiks = Array.from(randomBytes(8),
    (b) => ALIFBO[b % 36]).join("");
  const sir = randomBytes(32).toString("base64url");
  const [q] = await this.#sorov<{ id: number }>(
    `INSERT INTO api_kalitlar (foydalanuvchi_id, prefiks,
       kalit_hmac, nom, ruxsatlar, tugash_vaqti)
     VALUES ($1, $2, $3, $4, $5, CASE WHEN $6::int IS NULL
       THEN NULL ELSE now() + $6::int * interval '1 day' END)
     RETURNING id`,
    [fid, prefiks, this.#hmac(sir), nom, ruxsatlar,
      muddatKun ?? null]);
  if (q === undefined) throw new Error("kalit yaratilmadi");
  return { id: q.id, prefiks, kalit: `vz_${prefiks}_${sir}` };
}

/** Bearer kalit → egasi va ruxsatlari (faol, muddati o'tmagan,
 * egasi bloklanmagan); oxirgi_ishlatish 60 s qadam bilan */
async tekshir(kalit: string) {
  const m = KALIT.exec(kalit);
  if (m === null) return undefined;
  const [q] = await this.#sorov<{
    id: number; fid: number; h: Buffer; r: Ruxsat[]; eski: boolean;
  }>(
    `SELECT k.id, k.foydalanuvchi_id AS fid, k.kalit_hmac AS h,
       k.ruxsatlar AS r, coalesce(k.oxirgi_ishlatish
         < now() - interval '60 seconds', true) AS eski
     FROM api_kalitlar k
     JOIN foydalanuvchilar f ON f.id = k.foydalanuvchi_id
     WHERE k.prefiks = $1 AND k.bekor_vaqti IS NULL
       AND (k.tugash_vaqti IS NULL OR k.tugash_vaqti > now())
       AND f.bloklangan_vaqt IS NULL`, [m[1]]);
  if (q === undefined ||
    !timingSafeEqual(q.h, this.#hmac(m[2] ?? ""))) return undefined;
  if (q.eski) {
    await this.#pool.query(
      "UPDATE api_kalitlar SET oxirgi_ishlatish = now() " +
        "WHERE id = $1", [q.id]);
  }
  return { id: q.id, foydalanuvchiId: q.fid, ruxsatlar: q.r };
}

/** Aylantirish: yangi kalit, eskisi `oraliqSoat` dan keyin o'ladi */
async aylantir(fid: number, id: number, oraliqSoat = 24) {
  const [e] = await this.#sorov<{ nom: string; r: Ruxsat[] }>(
    `UPDATE api_kalitlar SET tugash_vaqti = LEAST(
       coalesce(tugash_vaqti, 'infinity'),
       now() + $3 * interval '1 hour')
     WHERE id = $1 AND foydalanuvchi_id = $2
       AND bekor_vaqti IS NULL
     RETURNING nom, ruxsatlar AS r`, [id, fid, oraliqSoat]);
  return e === undefined ? undefined : this.yarat(fid, e.nom, e.r);
}

Uch nozik joy:

  • timingSafeEqual(q.h, ...) — ikkala tomon ham 32 bayt (bazadagi CHECK buni kafolatlaydi), uzunlik xatosi bo'lmaydi.
  • f.bloklangan_vaqt IS NULL — egasi bloklansa, uning kalitlari ham darhol o'ladi. Oldingi darsdagi "bloklangan hech qayerdan kirmaydi" qoidasi kalitlarga ham tarqaldi.
  • LEAST(coalesce(tugash_vaqti, 'infinity'), ...) — kalitning muddati 24 soatdan qisqa bo'lsa, aylantirish uni uzaytirmaydi.

Ikkinchi qism — servislararo imzo middleware'i (to'liq, bo'lingan):

ts
/**
 * Imzolanadigan satr: METOD \n YO'L(+query) \n VAQT(unix s) \n
 * NONCE \n sha256(tana) hex. Sarlavhalar: X-Vaqt, X-Nonce, X-Imzo.
 * Himoya: soxtalashtirish (HMAC), tanani o'zgartirish (hash), qayta
 * o'ynatish (vaqt oynasi ±5 daqiqa + nonce oyna davomida).
 */
export function imzoSatri(metod: string, yol: string, vaqt: string,
  nonce: string, tana: Buffer | string): string {
  const h = createHash("sha256").update(tana).digest("hex");
  return [metod.toUpperCase(), yol, vaqt, nonce, h].join("\n");
}

export function hmacImzo(sir: Buffer, satr: string): string {
  return createHmac("sha256", sir).update(satr).digest("hex");
}

export function servisImzosi(sir: Buffer, oynaS = 300,
  soat: () => number = Date.now): RequestHandler {
  // nonce → muddati; bitta jarayon (ko'p nusxada — Redis SET NX)
  const korilgan = new Map<string, number>();
  return (req, _res, next) => {
    const vaqt = req.get("x-vaqt") ?? "";
    const nonce = req.get("x-nonce") ?? "";
    const imzo = req.get("x-imzo") ?? "";
    if (!/^\d{10}$/.test(vaqt) ||
      !/^[A-Za-z0-9_-]{16,64}$/.test(nonce) ||
      !/^[0-9a-f]{64}$/.test(imzo)) {
      throw new ImzoXatosi("sarlavhalar");
    }
    const hozir = Math.floor(soat() / 1000);
    if (Math.abs(hozir - Number(vaqt)) > oynaS) {
      throw new ImzoXatosi("vaqt");
    }
    const satr = imzoSatri(req.method, req.originalUrl, vaqt, nonce,
      req.xomTana ?? Buffer.alloc(0));
    const kutilgan = Buffer.from(hmacImzo(sir, satr), "hex");
    if (!timingSafeEqual(kutilgan, Buffer.from(imzo, "hex"))) {
      throw new ImzoXatosi("imzo");
    }
    for (const [k, t] of korilgan) if (t < hozir) korilgan.delete(k);
    if (korilgan.has(nonce)) throw new ImzoXatosi("takror");
    korilgan.set(nonce, hozir + 2 * oynaS);
    next();
  };
}

Tartibga e'tibor bering: nonce imzo tekshirilgandan keyin yoziladi. Aks holda hujumchi soxta imzoli so'rovlar bilan haqiqiy nonce'larni oldindan "band qilib" qo'yardi. ImzoXatosi — 401, kod: "imzo-xato". oynaS va soat parametrlari — test uchun: vaqtni boshqarib, "6 daqiqa eski" so'rovni kutmasdan sinash mumkin.

8.3 Ulanish

  • middleware.ts — kimlik(sessiyalar, apiKalitlar): Authorization bo'lsa — kalit (yaroqsiz → 401 + WWW-Authenticate: Bearer), aks holda cookie. kirishKerak — kalit bo'lsa metod bo'yicha ruxsat (yetmasa 403), keyin req.egasi = kalit egasi.
  • marshrut.ts — GET/POST /v1/auth/api-kalitlar, DELETE .../:id, POST .../:id/aylantirish; hammasi sessiyaBilan(req) orqali: req.apiKalit bo'lsa ham 401. Yaratish javobida ogohlantirish: "Kalit faqat hozir ko'rsatiladi — xavfsiz joyga saqlang".
  • ilova.ts — express.json({ verify }) (xom tana) va ICHKI_HMAC_SIRI berilsa POST /v1/ichki/tekshiruv (servisImzosi bilan). 29-qismdagi bot shu naqsh bilan ulanadi.

8.4 Testlar

test/api-kalit.test.ts (+6). Biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z PostgreSQL 17.6 bazamiz):

bash
node --test test/api-kalit.test.ts

Birinchi to'plamda bitta to'g'ri va beshta buzilgan so'rovga, qolganlarida status kodlarga qarang:

text
▶ servislararo HMAC imzo
  ✔ to'g'ri imzo — 200; boshqa sir, o'zgartirilgan tana, 6 daqiqa eski, takror nonce, sarlavhasiz — 401 imzo-xato (147.1714ms)
✔ servislararo HMAC imzo (149.4089ms)
✔ yaratish: vz_<8>_<43>, bir marta; ro'yxatda faqat prefiks; bazada sir yo'q (HMAC) (620.107ms)
✔ ruxsatlar: oqish — GET 200, POST 403; yozish — POST 201 (begona Origin bilan ham — cookie yo'q, CSRF emas) (972.9673ms)
✔ kalit auth yo'llarini boshqara olmaydi: kalitlar, /men, admin — 401; yaroqsiz kalit — 401 + WWW-Authenticate Bearer (862.3278ms)
✔ bekor, aylantirish (eski 24 soat yashaydi), muddat, bloklangan egasi — 401 (804.2368ms)
✔ begona kalitni bekor qilish — 404 (IDOR) (1538.1998ms)
ℹ tests 6
ℹ suites 1
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7466.7212

Nima bo'ldi? 6 test o'tdi: kalit faqat bir marta ko'rinadi, ruxsat yetmasa 403, kalit o'zini boshqara olmaydi, imzoning har buzilishi — 401. Imzo testidagi bitta qator alohida e'tiborga loyiq: {"vazifa": 1} — bitta qo'shimcha bo'shliq bilan — 401. Oxirgi test esa oldingi darsdagi IDOR shablonining yangi resursga qo'llanishi.

Kanon tekshiruvi (2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 210 (209 + 1 o'tkazilgan), Testcontainers 10, e2e 6/6, migratsiyalar sql/ bilan bir xil, jonli API 18/18 uch rejimda haqiqiy rollar bilan.

8.5 Commit

Keyingi dars (Auth.js) boshqa loyiha nusxasida tajriba — vazifalar-api ga hech narsa qo'shmaydi. Shuning uchun holat shu yerda commit qilinadi:

bash
M="feat(auth): API kalitlari (prefiks + HMAC, ruxsatlar,"
M+=" aylantirish) va servislararo HMAC imzo (vaqt oynasi, nonce);"
M+=" db: 121"
git add src test sql prisma
git commit -m "$M"

9. Real ishda

  • Stripe, GitHub, OpenAI kabi API'lar kalitni prefiks bilan beradi va faqat bir marta ko'rsatadi; ko'pchiligida ruxsatlar va muddat bor.
  • Webhook imzolari (Stripe, Telegram, to'lov tizimlari) — xuddi shu HMAC + vaqt naqshi, faqat yo'nalish teskari.
  • Bulut xizmatlari (AWS Signature V4) har so'rovni metod, yo'l, sarlavhalar, tana hash'i va vaqt bilan imzolaydi — bizning servislararo imzoning katta ko'rinishi.
  • Intervyuda: "API kalitni qanday saqlaysiz?", "Nega API kalitga argon2 shart emas?", "Takroriy hujumdan (replay attack) qanday himoyalanasiz?", "Bearer so'rovga CSRF kerakmi?".

Xulosa

  • API kalit = tur belgisi + ochiq prefiks + 256 bitlik sir; to'liq kalit faqat bir marta ko'rsatiladi.
  • Bazada — sirning HMAC'i (argon2 shart emas); tekshiruv: format → prefiks → timingSafeEqual; hamma xatoga bir xil 401.
  • Kalitga tor ruxsatlar, muddat, aylantirish (24 soatlik oraliq); kalitlarni faqat sessiya boshqaradi.
  • Authorization: Bearer — cookie emas, shuning uchun CSRF tekshiruvi kerak emas; yaroqsiz kalit — darhol 401 + WWW-Authenticate.
  • Servislararo imzo: metod, yo'l, vaqt, nonce va xom tana hash'i; vaqt oynasi + nonce — takroriy hujumga qarshi; ko'p nusxada nonce Redis'da.

Keyingi dars: Autentifikatsiya I: Auth.js (NextAuth v5) — Next.js ilovasiga tayyor kutubxona bilan kirish tizimini qo'shamiz va uni o'zimiz qurgan tizim bilan solishtiramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
API kalitlar va servislararo autentifikatsiya: prefiks, HMAC, ruxsatlar va imzoli so'rovlar — IlmHamroh