Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 Dastur odam emas
- 1.2 Ikki xil vaziyat
- 2. API kalitning tuzilishi
- 2.1 Prefiks va sir
- 2.2 Bazada — faqat HMAC
- 2.3 Jadval va cheklovlar
- 3. Ruxsatlar, muddat va aylantirish
- 3.1 Ruxsatlar (scope)
- 3.2 Muddat va oxirgi ishlatish
- 3.3 Aylantirish
- 3.4 Bearer va CSRF
- 4. Servislararo autentifikatsiya
- 4.1 Kalit yubormasdan isbotlash
- 4.2 Nima imzolanadi
- 4.3 Xom tana tuzog'i
- 4.4 Vaqt oynasi va nonce
- 4.5 Boshqa yo'llar: client credentials va mTLS
- 5. Hujumchi nigohi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): imzolovchi mijoz
- 3-mashq (qiyin): ruxsat va muddat
- 8. Loyiha qadami: API kalitlari va ichki imzo
- 8.1 Migratsiya va sozlamalar
- 8.2 src/auth/api-kalit.ts
- 8.3 Ulanish
- 8.4 Testlar
- 8.5 Commit
- 9. Real ishda
- Xulosa
- Manbalar
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: BearervaWWW-Authenticatesarlavhalarini to'g'ri ishlatasiz.vazifalar-apiga 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:
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 uchunghp_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.
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:
55 43
true
8 8 7 7Kalit 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.
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:
AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tk
32
egasi 1
sir xato
prefiks yo'q
formatBazada 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:
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:
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:
id | korinish | ruxsatlar | hmac_bayt
----+---------------+-------------------+-----------
1 | vz_k7m2x9qa_… | {vazifalar:oqish} | 32
(1 row)
INSERT 0 1sha256(...) — faqat 32 baytlik namuna qiymat; haqiqiy HMAC'ni ilova hisoblaydi. Ro'yxat yo'li aynan shunday korinish qaytaradi — sirsiz. Endi noma'lum ruxsat:
\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:
ERROR: new row for relation "api_kalitlar" violates check constraint "api_kalitlar_ruxsatlar_check"Va oldingi darsdagi qoida — ma'lumot roli kalitlarni ko'rmaydi:
SET ROLE vazifalar_api;
SELECT count(*) FROM api_kalitlar;Natija:
SET
ERROR: permission denied for table api_kalitlarMigratsiyada 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):
- yangi kalit yaratiladi (o'sha nom va ruxsatlar bilan);
- eskisining
tugash_vaqti24 soatdan keyinga qo'yiladi; - 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:
GET /v1/vazifalar HTTP/1.1
Authorization: Bearer vz_k7m2x9qa_AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tkYaroqsiz kalitga javob — 401 va RFC 6750 dagi sarlavha:
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:
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.
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:
64 475324dfe41c1810
5431c6b4325f3ac6 false
dff3d74659dc471f false
d137d96c5ab5d2e7Ikkinchi 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:
// 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:
- Vaqt oynasi:
X-Vaqtserver soatidan ±300 soniyadan ko'p farq qilsa — rad. Eski so'rovni ertaga yuborib bo'lmaydi. - Nonce: oyna ichida ham har nonce bir marta. Server ko'rilgan nonce'larni oyna davomida eslab qoladi.
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:
ok
takror
vaqt
ok
okBeshinchi 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-xato4.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_idvaclient_secretbilan 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
- API kalitning siri parol kabi argon2id bilan hash qilinishi kerak.
- Prefiks sir emas, uni loglarga yozish mumkin.
- Bearer kalit bilan keladigan so'rovda CSRF tekshiruvi shart.
- Servislararo imzoda tana hash'i satrga kirmasa, tanani almashtirish mumkin.
- Nonce'ni abadiy saqlash shart, aks holda eski so'rov qayta o'tadi.
Yechim
- Noto'g'ri — 256 bitlik tasodifiy sirni taxmin qilib bo'lmaydi; HMAC yetarli va tez.
- To'g'ri — u faqat "qaysi kalit"; sirsiz hech narsa bermaydi.
- Noto'g'ri — brauzer
Authorizationni o'zi qo'shmaydi, CSRF cookie bilan bog'liq. - To'g'ri — imzo faqat metod, yo'l, vaqt va nonce'ni himoya qilardi.
- 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
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:
X-Vaqt, X-Nonce, X-Imzo 64
true
false
falseUchinchi 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
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:
200
403
401
401
200Tartib 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:
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 (bazadagiCHECKbuni 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):
/**
* 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):Authorizationbo'lsa — kalit (yaroqsiz → 401 +WWW-Authenticate: Bearer), aks holda cookie.kirishKerak— kalit bo'lsa metod bo'yicha ruxsat (yetmasa 403), keyinreq.egasi = kalit egasi.marshrut.ts—GET/POST /v1/auth/api-kalitlar,DELETE .../:id,POST .../:id/aylantirish; hammasisessiyaBilan(req)orqali:req.apiKalitbo'lsa ham 401. Yaratish javobidaogohlantirish: "Kalit faqat hozir ko'rsatiladi — xavfsiz joyga saqlang".ilova.ts—express.json({ verify })(xom tana) vaICHKI_HMAC_SIRIberilsaPOST /v1/ichki/tekshiruv(servisImzosibilan). 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):
node --test test/api-kalit.test.tsBirinchi to'plamda bitta to'g'ri va beshta buzilgan so'rovga, qolganlarida status kodlarga qarang:
▶ 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.7212Nima 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:
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
- RFC 6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage" (
WWW-Authenticate,invalid_token): https://www.rfc-editor.org/rfc/rfc6750 - RFC 6749, "Client Credentials Grant" bo'limi: https://www.rfc-editor.org/rfc/rfc6749#section-4.4 ; RFC 8705 "OAuth 2.0 Mutual-TLS": https://www.rfc-editor.org/rfc/rfc8705
- OWASP REST Security Cheat Sheet (API kalitlar): https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- GitHub, token formatlari va prefikslar: https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
- Express
express.json()verifyparametri: https://expressjs.com/en/5x/api.html#express.json - Kanon tajribasi (IlmHamroh, 2026-10-28): holat 22 tekshiruvi (Node 24.21.0, PostgreSQL 18.6). Darsdagi brauzer misollari, SQL so'rovlar va
api-kalit.test.ts— biz ishga tushirdik (2026-10-28, PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!