IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi16/31-dars29 daqiqa
Mundarija (37)

Telegram Login va Mini App initData: HMAC imzosini serverda tekshirish

Qisqacha: Telegram'ning rasmiy kirish usuli endi OpenID Connect. Lekin Mini App va ko'p mavjud saytlar eski (legacy) usulda ishlaydi — kanon shuni quradi. Bu usulda Telegram foydalanuvchi ma'lumotini (id, ism, auth_date) bot tokeni asosidagi HMAC-SHA256 bilan imzolab beradi. Server hamma maydonni alifbo tartibida kalit=qiymat qilib, \n bilan ulaydi va imzoni qayta hisoblaydi. Login Widget'da kalit — SHA256(bot_token), Mini App'da — HMAC_SHA256("WebAppData", bot_token): ikkalasi farq qiladi. Imzo to'g'ri bo'lsa ham, auth_date eski bo'lsa — rad. Telegram email bermaydi, shuning uchun kanon yangi hisob ochmaydi: Telegram avval mavjud hisobga ulanadi, keyin u bilan kiriladi.

Bu darsda

  • Login Widget ma'lumotining imzosini brauzerda va Node'da hisoblaysiz.
  • Widget va Mini App kalitlari nega farq qilishini ko'rsatasiz.
  • auth_date bilan takroriy hujumdan himoyalanasiz.
  • Tokensiz tekshiruv (Ed25519) qanday ishlashini hujjat bo'yicha bilasiz.
  • vazifalar-api ga Telegram'ni ulash va u bilan kirishni qo'shib, holatni commit qilasiz.

Oldin bilishingiz kerak: OAuth 2.0 va OpenID Connect nazariyasi, Google bilan kirish, crypto 1: tasodif, hash va HMAC, Telegram Bot API asoslari (bot tokeni nima ekani).

Oldingi darsda Google bilan kirishni qurdik: standart OIDC, id_token, discovery. Telegram'ning ko'p yillik kirish usuli — eski (legacy) usul — boshqacha. Unda token almashinuvi va redirect zanjiri yo'q. Telegram ma'lumotni o'zi imzolab, brauzer orqali beradi. Server faqat bitta ish qiladi: imzoni tekshiradi.

1. Nega bu kerak?

1.1 O'zbekistonda Telegram

O'zbekistonda ko'p odamning telefonida Telegram bor. Email esa ko'pchilik uchun "bir marta ochilgan va unutilgan" narsa. "Telegram orqali kirish" tugmasi bunday foydalanuvchiga eng qulay yo'l. vazifalar ning kelajakdagi Telegram boti uchun ham shu bog'lanish kerak bo'ladi: bot kimga xabar yuborayotganini bilishi lozim.

1.2 Ikki joy, bitta g'oya

Telegram ma'lumoti ikki joydan keladi:

  • Login Widget — oddiy saytdagi "Log in with Telegram" tugmasi. Foydalanuvchi Telegram'da tasdiqlaydi, sayt id, first_name, username, photo_url, auth_date va hash ni oladi.
  • Mini App — Telegram ichida ochiladigan web sahifa. Sahifa Telegram.WebApp.initData satrini oladi va serverga yuboradi.

Ikkalasida ham ma'lumot brauzer orqali keladi. Brauzerdan kelgan har narsani hujumchi o'zgartirishi mumkin. Shuning uchun Telegram unga imzo qo'shadi. Imzo kaliti bot tokenidan olinadi, bot tokeni esa faqat Telegram va sizning serveringizda.

2. Bugungi holat: ikki xil Telegram Login

Diqqat: Biz haqiqiy bot va haqiqiy Telegram ma'lumoti bilan sinamadik. Hamma algoritm — rasmiy hujjat bo'yicha (core.telegram.org, 2026-10-28 da o'qildi), testlar — o'zimiz yaratgan soxta token bilan.

Darsni yozish kunida Telegram hujjatida muhim o'zgarish bor. "Log In With Telegram" sahifasi endi OpenID Connect oqimini tasvirlaydi. HMAC imzoli Login Widget hujjati alohida manzilga (/widgets/login-legacy) arxivlangan. Darsda biz uni eski (legacy) usul deymiz. Yangi provayderning discovery hujjatini o'qib ko'rdik (biz ishga tushirdik, 2026-10-28; kerakli maydonlar):

bash
curl -s https://oauth.telegram.org/.well-known/openid-configuration
text
"issuer": "https://oauth.telegram.org",
"authorization_endpoint": "https://oauth.telegram.org/auth",
"token_endpoint": "https://oauth.telegram.org/token",
"jwks_uri": "https://oauth.telegram.org/.well-known/jwks.json",
"response_types_supported": ["code"],
"scopes_supported": ["openid", "phone", "profile",
  "telegram:bot_access", "ton:wallet"],
"code_challenge_methods_supported": ["plain", "S256"],

Bu oldingi ikki darsdagi standart oqim: faqat code, PKCE, JWKS. Hujjatga ko'ra client_id va secret BotFather'da beriladi, alohida userinfo endpoint'i yo'q — ma'lumot id_token ichida. E'tibor bering: email scope'i yo'q.

Kanon esa eski (legacy) usulni qurdi va bunga ikki sabab bor:

  1. Mini App initData imzosi hamon HMAC — bu hujjatda o'zgarmagan. Telegram ichidagi ilovalar uchun bu yagona yo'l.
  2. Eski (legacy) vidjet bilan qurilgan saytlar ko'p, bunday kodni o'qish va tekshirish kerak bo'ladi. Arxivdagi hujjat vidjet o'chirilganini aytmaydi.

Yangi OIDC usulini kanon sinamadi. Uni qanday ulash mumkinligini darsning oxirida, «Real ishda» bo'limida ko'ramiz.

3. Login Widget imzosi

3.1 Algoritm

Eski (legacy) vidjet hujjatidagi qadamlar:

  1. hash dan boshqa hamma maydonni olamiz.
  2. Kalit bo'yicha alifbo tartibida saralaymiz.
  3. Har birini kalit=qiymat qilib, \n (yangi qator, 0x0A) bilan ulaymiz — bu data_check_string (tekshiruv satri).
  4. Sir kalit: secret_key = SHA256(bot_token).
  5. hex(HMAC_SHA256(secret_key, data_check_string)) — hash ga teng bo'lishi kerak.

3.2 Brauzerda sinab ko'ring

Soxta token bilan "Telegram serveri" va "bizning server" rolini birdaniga o'ynaymiz:

js
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
  (x) => x.toString(16).padStart(2, "0")).join("");

// SOXTA token — faqat dars uchun (haqiqiysi BotFather'dan, .env da)
const TOKEN = "7000000001:SoxtaToken_faqat-dars-uchun-0123456789";

function tekshiruvSatri(maydon) {
  return Object.keys(maydon)
    .filter((k) => k !== "hash")
    .sort()
    .map((k) => `${k}=${maydon[k]}`)
    .join("\n");
}

async function widgetHash(maydon, token) {
  const sir =
    await crypto.subtle.digest("SHA-256", enc.encode(token));
  const kalit = await crypto.subtle.importKey("raw", sir,
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const satr = enc.encode(tekshiruvSatri(maydon));
  return hex(await crypto.subtle.sign("HMAC", kalit, satr));
}

// "Telegram serveri" shunday imzolaydi
const maydon = {
  id: "123456789", first_name: "Aziz", username: "aziz_uz",
  auth_date: "1800000000",
};
maydon.hash = await widgetHash(maydon, TOKEN);
console.log(tekshiruvSatri(maydon));
console.log(maydon.hash.slice(0, 16));

// server tekshiradi
console.log(await widgetHash(maydon, TOKEN) === maydon.hash);
const soxta = { ...maydon, id: "1" };
console.log(await widgetHash(soxta, TOKEN) === soxta.hash);
const boshqaBot = "7000000002:BoshqaToken_faqat-dars-uchun-012345678";
console.log(await widgetHash(maydon, boshqaBot) === maydon.hash);

Konsolda:

text
auth_date=1800000000
first_name=Aziz
id=123456789
username=aziz_uz
e63e759100df6c93
true
false
false
  • Birinchi to'rt qator — tekshiruv satri. auth_date birinchi: alifbo bo'yicha a < f < i < u.
  • hash ning boshi (to'liq — 64 hex belgi).
  • To'g'ri ma'lumot — true.
  • Hujumchi id ni o'zgartirdi (boshqa odam bo'lib kirmoqchi) — false. Yangi imzo yasash uchun unga bot tokeni kerak.
  • Boshqa botning tokeni bilan — false. Bir botga berilgan ma'lumot boshqa botning saytida o'tmaydi.

3.3 Nega tokenning o'zi emas, uning hash'i

Telegram HMAC kaliti sifatida tokenning o'zini emas, SHA256(token) ni ishlatadi. Bu tokenni to'g'ridan-to'g'ri "kalit" sifatida ko'p joyda ishlatmaslik uchun. Bot tokeni — botingizning to'liq paroli: u bilan bot nomidan xabar yuborish mumkin. Kalitni undan alohida qoida bilan hosil qilish — oldingi darsdagi HKDF g'oyasining soddaroq ko'rinishi: bir sir, har maqsadga boshqa kalit.

Tekshirib ko'ring: Frontend Telegram vidjetidan kelgan first_name ni to'g'ridan-to'g'ri ekranga chiqarsa-chi? Imzo to'g'ri bo'lsa, xavf yo'qmi?

Javob

Imzo faqat "bu qiymatni Telegram bergan" deydi. Qiymatning o'zi xavfsiz degani emas. Foydalanuvchi Telegram'dagi ismini <img src=x onerror=…> qilib qo'yishi mumkin — Telegram uni imzolab beradi. Ekranga chiqarishda React kabi avtomatik ekranlash yoki HTML ekranlash baribir kerak (Tokenni qayerda saqlash va CSRF darsidagi XSS qoidalari).

4. auth_date: imzo eskirmaydi

4.1 Takroriy hujum

HMAC imzo vaqtni bilmaydi. Bugun berilgan ma'lumot bir yildan keyin ham "to'g'ri imzolangan" bo'lib qoladi. Kimdir uni bir marta qo'lga kiritsa — jurnal faylidan, ekran yozuvidan, ochiq qolgan noutbukdan — o'sha foydalanuvchi nomidan qayta-qayta kiradi. Bu takroriy hujum (replay attack).

Himoya — auth_date (Unix vaqti, soniyada). Hujjat uni tekshirishni talab qiladi, lekin chegarani aytmaydi. Kanon tanlovi:

  • sukut bo'yicha 1 kun (TELEGRAM_AUTH_MUDDAT_S=86400, sozlanadi: 60 soniyadan 7 kungacha);
  • kelajakdagi vaqt ham rad, agar 60 soniyadan ko'p farq qilsa — server soati biroz orqada bo'lishi mumkin, lekin bir soat emas.

4.2 Bir kun — ko'pmi?

Bu halol muhokama qilinadigan joy. Login Widget ma'lumoti foydalanuvchi tugmani bosgan zahoti serverga yuboriladi. Haqiqiy kirish uchun bir necha soniya yetadi. Bir kunlik oyna hujumchiga ham bir kun beradi. Kanon kirishni va ulashni shu oyna bilan tekshiradi. Sizning loyihangizda 5–10 daqiqa ancha xavfsizroq. Bir kun esa faqat Mini App kabi uzoq ochiq turadigan sahifalarda kerak bo'ladi.

Muddatdan tashqari kanonda yana bir himoya yo'q: bitta imzolangan ma'lumot oyna ichida bir necha marta ishlatilishi mumkin. To'liq yopish uchun ishlatilgan hash larni muddat tugaguncha saqlash kerak — xuddi bir martalik tokenlar kabi (Email tasdiqlash va parol tiklash oqimlari). Bu — 2-mashqdan keyingi o'ylash uchun savol.

5. Mini App initData

5.1 Boshqa kalit

Mini App sahifasi serverga initData satrini yuboradi — URL so'rov qatori ko'rinishida: auth_date=…&query_id=…&user=%7B…%7D&hash=…. user maydoni — JSON. Tekshiruv satri xuddi vidjetdagidek yig'iladi. Farq faqat kalitda:

Login Widget Mini App
Kalit SHA256(bot_token) HMAC_SHA256(kalit="WebAppData", ma'lumot=bot_token)
Ma'lumot shakli alohida maydonlar bitta query satr, user — JSON
Qayerdan saytdagi vidjet Telegram.WebApp.initData

Bu farq ataylab qilingan: vidjet uchun imzolangan ma'lumot Mini App sifatida o'tmasin va aksincha. Kanon testi buni alohida tekshiradi.

5.2 Ikki kalit yonma-yon

js
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
  (x) => x.toString(16).padStart(2, "0")).join("");
const TOKEN = "7000000001:SoxtaToken_faqat-dars-uchun-0123456789";

async function hmac(kalitBaytlari, matn) {
  const k = await crypto.subtle.importKey("raw", kalitBaytlari,
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  return crypto.subtle.sign("HMAC", k, enc.encode(matn));
}

// Login Widget: kalit = SHA256(token)
const widgetKalit =
  await crypto.subtle.digest("SHA-256", enc.encode(TOKEN));
// Mini App: kalit = HMAC(kalit "WebAppData", ma'lumot token)
const miniAppKalit = await hmac(enc.encode("WebAppData"), TOKEN);
console.log(hex(widgetKalit).slice(0, 16));
console.log(hex(miniAppKalit).slice(0, 16));

async function initDataTekshir(initData, token) {
  const p = new URLSearchParams(initData);
  const hash = p.get("hash");
  p.delete("hash");
  const satr = [...p].sort(([a], [b]) => (a < b ? -1 : 1))
    .map(([k, v]) => `${k}=${v}`).join("\n");
  const sir = await hmac(enc.encode("WebAppData"), token);
  return hex(await hmac(sir, satr)) === hash;
}

const user = JSON.stringify({ id: 42, first_name: "Malika" });
const maydon = { auth_date: "1800000000", query_id: "Q1", user };
const satr = Object.keys(maydon).sort()
  .map((k) => `${k}=${maydon[k]}`).join("\n");

const togri = hex(await hmac(miniAppKalit, satr));
const widgetcha = hex(await hmac(widgetKalit, satr));
const q = (hash) =>
  new URLSearchParams({ ...maydon, hash }).toString();
console.log(await initDataTekshir(q(togri), TOKEN));
console.log(await initDataTekshir(q(widgetcha), TOKEN));
console.log(q(togri).slice(0, 52));

Konsolda:

text
78413e509fcfae17
0e92ddfcad385fe3
true
false
auth_date=1800000000&query_id=Q1&user=%7B%22id%22%3A
  • Bir token — ikki butunlay boshqa kalit.
  • Mini App kaliti bilan imzolangan initData — o'tdi.
  • Xuddi shu ma'lumot, lekin vidjet kaliti bilan imzolangan — rad.
  • Oxirgi qator: initData serverga shu ko'rinishda keladi. user dagi JSON URL-kodlangan (%7B = {). URLSearchParams uni avtomatik ochadi, tekshiruv satriga ochilgan qiymat kiradi.

Diqqat qiling: HMAC_SHA256("WebAppData", token) da kalit — doimiy "WebAppData" satri, ma'lumot — token. Ko'p dasturchi ikkalasini almashtirib qo'yadi va imzo hech qachon mos kelmaydi. Mini App backend'ini batafsil — middleware, sessiya, to'lovlar bilan — Telegram Mini App backendi va Stars darsida ko'rgansiz. Bugungi maqsad — auth tomoni.

5.3 initDataUnsafe ga ishonmang

Mini App sahifasida Telegram.WebApp.initDataUnsafe ham bor — o'sha ma'lumotning tayyor obyekt ko'rinishi. Nomi o'zi aytib turibdi: xavfli. U faqat ekranda ism ko'rsatish uchun. Server hech qachon frontend'dan kelgan user.id ga ishonmaydi. U faqat imzosi tekshirilgan initData dan o'zi ajratib olgan qiymatga ishonadi.

6. Tokensiz tekshiruv: Ed25519

6.1 Nima uchun kerak

Ba'zan initData ni bot egasi bo'lmagan tomon tekshirishi kerak. Masalan, Mini App boshqa kompaniyaning to'lov xizmatiga murojaat qiladi. Unga bot tokenini berib bo'lmaydi — token bilan botni boshqarish mumkin. Shuning uchun Telegram initData ga yana bitta maydon qo'shgan: signature. Bu — Telegram'ning shaxsiy kaliti bilan qo'yilgan Ed25519 imzosi. Uni Telegram'ning ochiq kaliti bilan istalgan kishi tekshira oladi, lekin hech kim soxtalashtira olmaydi.

Hujjat bo'yicha tekshiruv satri boshqacha:

  • boshida <bot_id>:WebAppData va yangi qator;
  • keyin hash va signature dan tashqari hamma maydon, alifbo tartibida;
  • imzo — base64url;
  • ochiq kalitlar hujjatda: production uchun e7bf03a2…242d, test muhiti uchun 40055058…72ec (to'liq qiymat — kanon kodida).

6.2 Node'da

Brauzerdagi Web Crypto'da Ed25519 hamma joyda bir xil emas, shuning uchun Node'da ko'ramiz. Telegram'ning shaxsiy kaliti bizda yo'q. Shuning uchun o'z kalit juftligimizni yasaymiz va Telegram rolini o'ynaymiz:

js
import { createPrivateKey, createPublicKey, sign, verify }
  from "node:crypto";

// Ed25519 kalitlarini 32 baytlik "urug'"dan yasaymiz — natija
// takrorlansin. Bu O'ZIMIZNING kalit (Telegram'niki emas)
const urug = Buffer.alloc(32, 7);
const PKCS8 = Buffer.from("302e020100300506032b657004220420", "hex");
const shaxsiy = createPrivateKey({
  key: Buffer.concat([PKCS8, urug]), format: "der", type: "pkcs8",
});
const ochiqHex = createPublicKey(shaxsiy)
  .export({ format: "der", type: "spki" })
  .subarray(12).toString("hex");
console.log(ochiqHex.length, ochiqHex.slice(0, 16));

// Telegram hujjatidagi kabi: hex (32 bayt) → SPKI DER → kalit
const SPKI = Buffer.from("302a300506032b6570032100", "hex");
const ochiqKalit = (hex) => createPublicKey({
  key: Buffer.concat([SPKI, Buffer.from(hex, "hex")]),
  format: "der", type: "spki",
});

const satr = (botId) =>
  `${botId}:WebAppData\nauth_date=1800000000\nuser={"id":42}`;
const imzo = sign(null, Buffer.from(satr(7000000001)), shaxsiy);
console.log(imzo.length, imzo.toString("base64url").slice(0, 16));

const tekshir = (botId, hex) =>
  verify(null, Buffer.from(satr(botId)), ochiqKalit(hex), imzo);
console.log(tekshir(7000000001, ochiqHex));
console.log(tekshir(7000000002, ochiqHex));

const PROD =
  "e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d";
console.log(ochiqKalit(PROD).asymmetricKeyType);
console.log(tekshir(7000000001, PROD));

Konsolda:

text
64 ea4a6c63e29c520a
64 nGpt9e9e25eihWO8
true
false
ed25519
false
  • Ochiq kalit — 32 bayt (64 hex belgi), imzo — 64 bayt.
  • O'z ochiq kalitimiz bilan imzo o'tdi.
  • Boshqa bot_id bilan — rad: bot_id tekshiruv satri ichida. Bir botning ma'lumoti boshqa bot nomidan o'tmaydi.
  • Telegram'ning production kaliti Node uchun to'g'ri Ed25519 kalit bo'lib o'qildi. Bizning imzomizni u rad etdi — imzo Telegram'niki emas, kutilgan natija.

Node 32 baytlik "xom" kalitni to'g'ridan-to'g'ri qabul qilmaydi — unga standart SPKI konvert kerak. 302a300506032b6570032100 — Ed25519 ochiq kaliti uchun o'sha 12 baytlik sarlavha. Kanon kodi aynan shunday qiladi.

6.3 Halol noaniqlik

Yangi initData da signature maydoni ham keladi. Bot tomondagi HMAC tekshiruvi uchun hujjat tekshiruv satrini "barcha qabul qilingan maydonlar zanjiri" deydi. Demak, signature HMAC satriga kiradi va faqat hash chiqariladi. Kanon kodi aynan shunday qiladi. Lekin biz buni haqiqiy Telegram'dan kelgan initData bilan tekshirmadik. Haqiqiy botda birinchi marta ishlatganda shuni sinang.

Kanon qarori: o'z serveringizda bot tokeni bor — HMAC usuli asosiy. Ed25519 tekshiruvi (initDataImzoTekshir) kodda bor va testlangan, lekin hech qaysi endpoint'ga ulanmagan. U kelajakdagi Mini App va uchinchi tomon xizmatlari uchun tayyor funksiya.

7. src/auth/telegram.ts

Uchala tekshiruv bitta modulda. Telefon uchun formatladik: qatorlar bo'lindi, ochiq kalitlar ikki satrga ajratildi, uch marta takrorlangan "hozirgi vaqt" sukut qiymati hozirgi() funksiyasiga olindi. Ma'no kanondagidek; bo'lingan nusxa tip, lint va testlardan o'tdi (biz tekshirdik):

ts
// telegram.ts — Telegram Login Widget va Mini App initData imzosini
// tekshirish — core.telegram.org hujjatidagi algoritmlar.
// Haqiqiy bot (BotFather) va token ishlatilmagan — testlar SOXTA
// token bilan imzolangan namunalar (SINALMAGAN: haqiqiy Telegram
// ma'lumoti).
//   Login Widget: secret_key = SHA256(bot_token);
//     hash = hex(HMAC_SHA256(secret_key, data_check_string)).
//   Mini App: secret_key = HMAC_SHA256(kalit "WebAppData",
//     xabar bot_token); hash — xuddi shunday.
//   data_check_string — "hash" dan tashqari hamma maydon, kalit
//     bo'yicha alifbo tartibida, "kalit=qiymat", "\n" bilan.
//   Third-party (tokensiz): signature = base64url Ed25519(
//     `${bot_id}:WebAppData\n` + hash va signature'siz maydonlar),
//     Telegram ochiq kaliti bilan.
// Muddat: auth_date eski bo'lsa — rad (o'g'irlangan/qayta o'ynatilgan
// ma'lumot). Solishtirish — timingSafeEqual.
import {
  createHash,
  createHmac,
  createPublicKey,
  timingSafeEqual,
  verify,
} from "node:crypto";

/** Telegram hujjatidagi ochiq kalitlar (hex, Ed25519) */
export const TELEGRAM_OCHIQ_KALIT = Object.freeze({
  production:
    "e7bf03a2fa4602af4580703d88dda5bb" +
    "59f32ed8b02a56c187fe7d34caed242d",
  test:
    "40055058a4ee38156a06562e52eece92" +
    "a771bcd8346a8c4615cb7376eddf72ec",
});

export type TelegramFoydalanuvchi = {
  id: number;
  ism: string;
  username?: string | undefined;
};
export type TekshiruvNatijasi =
  | {
      tur: "ok";
      foydalanuvchi: TelegramFoydalanuvchi;
      authDate: number;
    }
  | { tur: "xato"; sabab: "imzo" | "eskirgan" | "maydon" };

function tekshiruvSatri(
  maydonlar: ReadonlyMap<string, string>,
  tashla: readonly string[],
): string {
  return [...maydonlar]
    .filter(([k]) => !tashla.includes(k))
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, v]) => `${k}=${v}`)
    .join("\n");
}

function hexTeng(a: string, b: Buffer): boolean {
  if (!/^[0-9a-f]{64}$/.test(a)) return false;
  return timingSafeEqual(Buffer.from(a, "hex"), b);
}

function muddat(
  authDate: string | undefined,
  maksS: number,
  hozirS: number,
): number | undefined {
  if (authDate === undefined || !/^\d{1,12}$/.test(authDate)) {
    return undefined;
  }
  const t = Number(authDate);
  // kelajakdagi vaqt (soat farqi 60 s dan ko'p) ham rad
  return hozirS - t > maksS || t - hozirS > 60 ? undefined : t;
}

const hozirgi = (): number => Math.floor(Date.now() / 1000);

/** Login Widget: maydonlar (id, first_name, last_name, username,
 * photo_url, auth_date, hash) — JSON yoki query'dan, hammasi satr */
export function widgetTekshir(
  maydon: Record<string, string>,
  botToken: string,
  maksS: number,
  hozirS = hozirgi(),
): TekshiruvNatijasi {
  const m = new Map(Object.entries(maydon));
  const hash = m.get("hash");
  if (hash === undefined || m.get("id") === undefined) {
    return { tur: "xato", sabab: "maydon" };
  }
  const kalit = createHash("sha256").update(botToken).digest();
  const hisob = createHmac("sha256", kalit)
    .update(tekshiruvSatri(m, ["hash"]))
    .digest();
  if (!hexTeng(hash, hisob)) return { tur: "xato", sabab: "imzo" };
  const t = muddat(m.get("auth_date"), maksS, hozirS);
  if (t === undefined) return { tur: "xato", sabab: "eskirgan" };
  const id = Number(m.get("id"));
  if (!Number.isSafeInteger(id) || id <= 0) {
    return { tur: "xato", sabab: "maydon" };
  }
  const ism = [m.get("first_name"), m.get("last_name")]
    .filter((x) => x !== undefined && x !== "")
    .join(" ");
  return {
    tur: "ok",
    foydalanuvchi: {
      id,
      ism: ism === "" ? `Telegram ${String(id)}` : ism,
      username: m.get("username"),
    },
    authDate: t,
  };
}

/** Mini App: Telegram.WebApp.initData satri (URL-encoded query).
 * Hujjat: data_check_string'dan faqat "hash" chiqariladi */
export function initDataTekshir(
  initData: string,
  botToken: string,
  maksS: number,
  hozirS = hozirgi(),
): TekshiruvNatijasi {
  const m = new Map(new URLSearchParams(initData));
  const hash = m.get("hash");
  if (hash === undefined) return { tur: "xato", sabab: "maydon" };
  const kalit = createHmac("sha256", "WebAppData")
    .update(botToken)
    .digest();
  const hisob = createHmac("sha256", kalit)
    .update(tekshiruvSatri(m, ["hash"]))
    .digest();
  if (!hexTeng(hash, hisob)) return { tur: "xato", sabab: "imzo" };
  return natija(m, maksS, hozirS);
}

/** Third-party: bot tokenisiz — Telegram ochiq kaliti (Ed25519)
 * bilan `signature` maydoni */
export function initDataImzoTekshir(
  initData: string,
  botId: number,
  ochiqKalitHex: string,
  maksS: number,
  hozirS = hozirgi(),
): TekshiruvNatijasi {
  const m = new Map(new URLSearchParams(initData));
  const imzo = m.get("signature");
  if (imzo === undefined) return { tur: "xato", sabab: "maydon" };
  // Ed25519 ochiq kaliti (32 bayt) → SPKI DER (12 baytlik prefiks)
  const kalit = createPublicKey({
    key: Buffer.concat([
      Buffer.from("302a300506032b6570032100", "hex"),
      Buffer.from(ochiqKalitHex, "hex"),
    ]),
    format: "der",
    type: "spki",
  });
  const satr =
    `${String(botId)}:WebAppData\n` +
    tekshiruvSatri(m, ["hash", "signature"]);
  const togri = verify(
    null, Buffer.from(satr), kalit, Buffer.from(imzo, "base64url"));
  if (!togri) return { tur: "xato", sabab: "imzo" };
  return natija(m, maksS, hozirS);
}

function natija(
  m: ReadonlyMap<string, string>,
  maksS: number,
  hozirS: number,
): TekshiruvNatijasi {
  const t = muddat(m.get("auth_date"), maksS, hozirS);
  if (t === undefined) return { tur: "xato", sabab: "eskirgan" };
  let u: unknown;
  try {
    u = JSON.parse(m.get("user") ?? "null");
  } catch {
    return { tur: "xato", sabab: "maydon" };
  }
  if (
    typeof u !== "object" ||
    u === null ||
    !("id" in u) ||
    typeof u.id !== "number" ||
    !Number.isSafeInteger(u.id)
  ) {
    return { tur: "xato", sabab: "maydon" };
  }
  const ism =
    "first_name" in u && typeof u.first_name === "string"
      ? u.first_name
      : `Telegram ${String(u.id)}`;
  const username =
    "username" in u && typeof u.username === "string"
      ? u.username
      : undefined;
  return {
    tur: "ok",
    foydalanuvchi: { id: u.id, ism, username },
    authDate: t,
  };
}

Nimaga qarang:

  • Tartib: avval imzo, keyin muddat. Imzosiz ma'lumotdagi auth_date ga ishonib bo'lmaydi.
  • hexTeng — avval format ([0-9a-f]{64}), keyin timingSafeEqual. Buzuq hash ("ZZ") xato tashlamaydi, false qaytaradi. Uzunlik farqi ham shu regex bilan ushlanadi.
  • Tekshirilgandan keyin ham ishonmaslik. JSON.parse try ichida, user.id — xavfsiz butun sonmi, tekshiriladi. unknown va "id" in u — Custom type guard va assertion funksiyalar uslubi.
  • .sort taqqoslovchisi — localeCompare emas, oddiy <. Hujjat "alifbo tartibida" deydi; localeCompare esa foydalanuvchi tiliga qarab boshqacha saralashi mumkin, < doim bir xil.
  • Natija — { tur: "ok" } yoki { tur: "xato", sabab }. Xato tashlanmaydi: qaysi HTTP status qaytarishni xizmat qatlami hal qiladi.

8. Hisob siyosati: ulash, keyin kirish

8.1 Telegram email bermaydi

Google bilan kirishda yangi hisob ochish mumkin edi: email bor, tasdiqlangan. Telegram faqat id, ism va username beradi. vazifalar da esa email majburiy: tiklash xatlari, bildirishnomalar. Kanon qarori:

  • Telegram bilan yangi hisob ochilmaydi;
  • tizimga kirgan foydalanuvchi sozlamalarda Telegram'ni ulaydi;
  • shundan keyin Telegram bilan kirish mumkin.
sequenceDiagram
  participant B as Brauzer
  participant A as vazifalar-api
  participant D as Postgres
  Note over B,A: 1-marta: email bilan kirgan
  B->>A: POST /v1/auth/telegram/ulash (vidjet + cookie)
  A->>A: imzo va auth_date
  A->>D: tashqi_hisoblar: telegram, id
  A-->>B: 204
  Note over B,A: keyingi safar
  B->>A: POST /v1/auth/telegram (vidjet)
  A->>A: imzo va auth_date
  A->>D: telegram id bo'yicha qidirish
  A-->>B: 200 + sessiya cookie

Oldingi darsdagi tashqi_hisoblar jadvali o'zgarishsiz ishlaydi: provayder = 'telegram', subyekt — Telegram id (satr ko'rinishida). Migratsiya kerak emas.

8.2 Xizmat qatlami

src/auth/xizmat.ts ga ikki metod va bitta yordamchi (telefon uchun bo'lindi, ikki marta takrorlangan xato parametrlari band o'zgaruvchisiga olindi):

ts
/**
 * Telegram Login Widget. Telegram email bermaydi — yangi hisob
 * ochilmaydi: avval tizimga kirgan foydalanuvchi Telegram'ni
 * ULAYDI (telegramUlash), keyin shu Telegram bilan kira oladi.
 * Imzo bot tokeni bilan tekshiriladi, auth_date eskirgan — rad.
 */
async telegramKirish(
  maydon: Record<string, string>,
  mijoz: Mijoz,
): Promise<Foydalanuvchi> {
  const id = telegramTekshir(maydon);
  const h = await ombor.tashqiBilan("telegram", id);
  if (h === undefined) {
    await ombor.hodisa({
      tur: "tashqi_rad", ...mijoz,
      tafsilot: { provayder: "telegram", sabab: "ulanmagan" },
    });
    throw new IlovaXatosi(404,
      "Bu Telegram hisobi hech bir hisobga ulanmagan — avval " +
      "email bilan kirib, sozlamalarda ulang", {
        kod: "telegram-ulanmagan",
        sarlavha: "Telegram ulanmagan",
      });
  }
  await ombor.hodisa({
    tur: "tashqi_kirish", foydalanuvchiId: h.id, ...mijoz,
    tafsilot: { provayder: "telegram" },
  });
  return { id: h.id, email: h.email, toliqIsm: h.toliqIsm };
},

async telegramUlash(
  foydalanuvchiId: number,
  maydon: Record<string, string>,
  mijoz: Mijoz,
): Promise<void> {
  const id = telegramTekshir(maydon);
  const band = {
    kod: "tashqi-band", sarlavha: "Boshqa hisob ulangan",
  };
  if ((await ombor.tashqiBilan("telegram", id)) !== undefined) {
    throw new KonfliktXatosi(
      "Bu Telegram hisobi allaqachon ulangan", band);
  }
  const ok = await ombor.tashqiBogla(
    foydalanuvchiId, "telegram", id, undefined, false);
  if (!ok) {
    throw new KonfliktXatosi(
      "Hisobingizga boshqa Telegram ulangan", band);
  }
  await ombor.hodisa({
    tur: "tashqi_boglandi", foydalanuvchiId, ...mijoz,
    tafsilot: { provayder: "telegram" },
  });
},

Va yordamchi funksiya (o'sha faylda, xizmat obyektidan tashqarida):

ts
/** Widget imzosi va muddati → Telegram id (satr); xato — 401 */
function telegramTekshir(maydon: Record<string, string>): string {
  if (d.telegram === undefined) {
    throw new IlovaXatosi(404, "Telegram kirish sozlanmagan", {
      kod: "provayder-yoq", sarlavha: "Provayder yo'q",
    });
  }
  const { botToken, maksS } = d.telegram;
  const n = widgetTekshir(maydon, botToken, maksS);
  if (n.tur === "xato") throw new TelegramXatosi(n.sabab);
  return String(n.foydalanuvchi.id);
}

TelegramXatosi — 401, kod telegram-xato. Uchta holatning statuslari:

Holat Status Kod
imzo yoki muddat noto'g'ri 401 telegram-xato
Telegram hech kimga ulanmagan 404 telegram-ulanmagan
Telegram boshqa hisobga ulangan 409 tashqi-band

tashqiBogla(…, false) — oxirgi argument "tasdiqlanmagan hisobni tozala" emas. Telegram ulashni tizimga kirgan foydalanuvchining o'zi qiladi — pre-hijacking bu yerda bo'lmaydi.

8.3 Sxema va marshrut

Telegram'ning JavaScript kodi id va auth_date ni son ko'rinishida beradi, qolganini satr. Imzo esa satr ko'rinishidan hisoblangan. Zod sxemasi ikkalasini qabul qilib, satrga aylantiradi (src/auth/sxema.ts):

ts
const TgQiymat = z
  .union([z.string().max(512), z.number().int().nonnegative()])
  .transform(String);
export const TelegramSxemasi = z.strictObject({
  id: TgQiymat,
  first_name: TgQiymat.optional(),
  last_name: TgQiymat.optional(),
  username: TgQiymat.optional(),
  photo_url: TgQiymat.optional(),
  auth_date: TgQiymat,
  hash: z.string().max(128),
  eslabQol: z.boolean().default(false),
});

strictObject — noma'lum maydon bilan so'rov 422 (Validatsiya: nima uchun, qayerda va qanday). Lekin bu yerda bir xavf ham bor: Telegram kelajakda vidjetga yangi maydon qo'shsa, u imzo satriga kiradi, sxema esa uni rad etadi. Bu "xavfsiz tomonga" xato — kirish ishlamay qoladi, lekin soxta kirish bo'lmaydi.

Marshrut (src/auth/marshrut.ts, telefon uchun bo'lindi):

ts
// Telegram Login Widget — kirish (ulangan hisob) va ulash (tizimga
// kirgan foydalanuvchi)
type Body = Record<string, string | boolean | undefined>;
const tgMaydon = (b: Body): Record<string, string> =>
  Object.fromEntries(Object.entries(b).filter(
    (x): x is [string, string] =>
      x[0] !== "eslabQol" && typeof x[1] === "string"));
r.route("/telegram")
  .post(jsonKerak, tekshirib({ body: TelegramSxemasi },
    async ({ body }, req, res) => {
      const f =
        await xizmat.telegramKirish(tgMaydon(body), mijoz(req));
      await sessiyaBer(req, res, f, body.eslabQol);
      res.json({ foydalanuvchi: f });
    }))
  .all(metodYoq("POST"));
r.route("/telegram/ulash")
  .post(jsonKerak, tekshirib({ body: TelegramSxemasi },
    async ({ body }, req, res) => {
      if (req.sessiya === undefined) throw new KirilmaganXatosi();
      await xizmat.telegramUlash(
        req.sessiya.foydalanuvchi.id, tgMaydon(body), mijoz(req));
      res.sendStatus(204);
    }))
  .all(metodYoq("POST"));

tgMaydon bizning eslabQol maydonimizni olib tashlaydi: u Telegram imzosiga kirmaydi. Shuni unutsangiz, tekshiruv satriga ortiqcha qator qo'shilib, hamma kirish 401 bo'lardi.

Google yo'llaridan farqi: bu yerda yo'naltirish yo'q, oddiy JSON POST. Vidjet ma'lumotni frontend'ga beradi (data-onauth callback rejimi), frontend uni API'ga yuboradi. POST bo'lgani uchun CSRF himoyasi (Tokenni qayerda saqlash va CSRF) bu yo'llarga ham avtomatik qo'llanadi.

8.4 Sozlama

text
# Telegram Login / Mini App (BotFather tokeni; bo'lmasa yo'l o'chiq)
# va auth_date muddati (s)
# TELEGRAM_BOT_TOKEN=
TELEGRAM_AUTH_MUDDAT_S=86400

src/config.ts tokenni format bo'yicha tekshiradi: ^\d{5,15}:[\w-]{30,60}$ — raqamli bot id, ikki nuqta va 30–60 belgili qism. Token berilmasa, ikkala yo'l 404 qaytaradi. Haqiqiy vidjet uchun BotFather'da botga saytingiz domenini bog'lash kerak (eski (legacy) hujjatda — /setdomain buyrug'i). Biz buni qilmadik.

Tekshirib ko'ring: Nega TELEGRAM_BOT_TOKEN noto'g'ri formatda bo'lsa, server umuman ishga tushmaydi, yo'l shunchaki o'chiq qolmaydi?

Javob

Token berilgan — demak administrator Telegram kirishini yoqmoqchi. Agar u nusxalashda xato qilgan bo'lsa (bo'sh joy, yarim token), server "ishlab turgandek" ko'rinadi, lekin hamma Telegram kirishi 401 beradi. Xatoni foydalanuvchilar topadi. Ishga tushishda darhol yiqilish (fail-fast) uni deploy paytida ko'rsatadi (Env validatsiyasi va tipli config obyekti).

9. Testlar

test/telegram.test.ts har ishga tushishda tasodifiy soxta token yasaydi: 7000000001: + 35 belgi. Format haqiqiy, lekin bunday bot yo'q. "Telegram serveri" rolini test o'zi o'ynaydi va shu token bilan imzolaydi. Uchta test bazasiz (algoritmlar), ikkitasi Postgres bilan (ulash → kirish oqimi).

Biz ishga tushirdik (kanon nusxasi va darsdagi bo'lingan fayllar bilan, Node 24.21.0, PostgreSQL 17.6):

bash
node --test test/telegram.test.ts

Bu safar testlarning bir qismi guruhda (▶ — guruh boshi). Belgilarga va oxirgi sonlarga qarang:

text
▶ imzo algoritmlari (bazasiz)
  ✔ Login Widget: to'g'ri — ok; maydon o'zgartirilsa, boshqa token, eskirgan, kelajakdagi, hash'siz — xato (3.7023ms)
  ✔ Mini App initData: Widget kaliti bilan imzolangani YAROQSIZ (kalit boshqacha hosil qilinadi); user JSON'dan (10.5133ms)
  ✔ third-party (tokensiz) Ed25519 imzo — o'zimiz yaratgan kalit juftligi bilan (Telegram ochiq kaliti — hujjatda) (5.2898ms)
✔ imzo algoritmlari (bazasiz) (20.8777ms)
✔ ulanmagan Telegram — 404; ulash (sessiya bilan) → keyin Telegram bilan kirish → o'sha hisob (687.0104ms)
✔ soxta imzo, eskirgan auth_date — 401 telegram-xato; boshqa hisobga ulangan Telegram — 409 (524.7601ms)
ℹ tests 5
ℹ suites 1
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2919.0962

Nima bo'ldi? Beshta test o'tdi (tests 5, suites 1 — bitta guruh). Guruhdagi uchtasi bazasiz: uchala imzo algoritmi. Qolgan ikkitasi Postgres bilan: ulash, kirish va xato statuslari.

Birinchi algoritm testining ichida yettita tekshiruv bor: to'g'ri ma'lumot, o'zgartirilgan id, boshqa token, 25 soat eski, 1 soat kelajakdagi, hash siz va buzuq hash. To'rtinchi testda id son ko'rinishida yuboriladi — Telegram'ning JavaScript kodi shunday beradi; sxema uni satrga aylantiradi va imzo o'tadi.

10. Loyiha qadami: tekshiruv va commit

Telegram kodi tayyor, testlar o'tdi. Commit'dan oldin butun loyihani tekshiring:

bash
npm run check

Kanon natijasi (2026-10-28, PostgreSQL 18.6, Node 24.21.0): tip va lint xatosiz, testlar 176 (175 + 1 o'tkazilgan) — beshta yangi Telegram testi. Testcontainers 10/10, e2e 4/4, Prisma migratsiyalari sql/ bilan bir xil, sxema farqi yo'q. Yangi migratsiya ham, yangi bog'liqlik ham yo'q — hammasi node:crypto.

bash
M="feat(auth): Telegram Login Widget bilan kirish va ulash;"
M+=" Mini App initData va Ed25519 (third-party) imzo tekshiruvi"
git add src test
git commit -m "$M"

Shu bilan vazifalar-api da to'rtta kirish usuli bor: parol, SMS kod, Google va Telegram. Keyingi ikki dars (OneID va auth kutubxonalari) loyiha kodini o'zgartirmaydi — ular reja, o'lchov va tanlov haqida.

11. Hujumchi nigohi

Hujum Nima uchun ishlaydi Himoya (kanonda) Qanday tekshirilgan
id ni almashtirib boshqa odam bo'lib kirish server imzoni tekshirmaydi HMAC, bot tokeni faqat serverda Widget testi; 5-test (soxta imzo — 401)
bitta botning ma'lumoti boshqa botga kalit umumiy kalit token'dan; Ed25519'da bot_id satrda Widget va Ed25519 testlari
vidjet imzosini Mini App sifatida bitta kalit ikki xil kalit hosil qilish initData testi
eski ma'lumotni qayta yuborish (takroriy hujum) imzo eskirmaydi auth_date ≤ 1 kun (sozlanadi) 25 soat eski — xato
kelajak vaqtli ma'lumot faqat "eski" tekshiriladi +60 s dan ortig'i rad 1 soat kelajak — xato
initDataUnsafe dan id olish frontend obyektiga ishonish faqat tekshirilgan initData kod ko'rigi (server bu obyektni olmaydi)
hash ni === bilan solishtirish vaqt hujumi timingSafeEqual kod ko'rigi
bot tokeni sizib chiqadi token — kalit manbai .env, git'da emas; BotFather'da yangilash kod himoya qilolmaydi

Oxirgi qator eng og'iri. Token sizsa, hujumchi istalgan foydalanuvchi nomidan vidjet ma'lumotini o'zi imzolaydi. Bunda hech qanday kod himoya qilmaydi. Yagona yo'l — BotFather'da tokenni darhol almashtirish (Sirlarni boshqarish). Kanonda yana bitta kuchsiz joy bor: muddat oynasida bitta ma'lumot bir necha marta ishlatilishi mumkin («auth_date: imzo eskirmaydi» bo'limi).

12. Ko'p uchraydigan xatolar

Xato Nega xato To'g'risi
HMAC(token, "WebAppData") kalit va ma'lumot almashgan kalit "WebAppData", ma'lumot token
vidjetda HMAC kaliti — token o'zi hujjat SHA256(token) deydi avval SHA-256
hash ni ham satrga qo'shish o'zini o'zi imzolay olmaydi hash chiqariladi
localeCompare bilan saralash lokalga bog'liq tartib oddiy <
user JSON'ini qayta stringify qilish bo'shliq va tartib o'zgaradi kelgan satrning o'zi
auth_date ni tekshirmaslik abadiy amal qiladigan ma'lumot muddat chegarasi
eslabQol kabi o'z maydonni satrga qo'shish imzo hech qachon mos kelmaydi faqat Telegram maydonlari
Telegram bilan yangi hisob ochish (emailsiz) tiklash yo'li yo'q ulash, keyin kirish

13. Mashqlar

Mashqlar kurs/mashqlar/28/16-telegram/ da.

1-mashq (oson): tekshiruv satri

Vidjetdan quyidagi maydonlar keldi: username=malika_uz, id=42, hash=ab…, first_name=Malika, auth_date=1800000000, photo_url=https://example.com/m.jpg. Tekshiruv satrini qo'lda yozing (har qator alohida). Keyin: agar frontend eslabQol=true ni ham qo'shib yuborsa va server uni olib tashlamasa, nima bo'ladi?

Yechim
text
auth_date=1800000000
first_name=Malika
id=42
photo_url=https://example.com/m.jpg
username=malika_uz

hash chiqarildi, qolgan beshtasi alifbo tartibida (a, f, i, p, u). photo_url dagi : va / o'zgarmaydi — satrga qiymat aynan kirganicha qo'shiladi.

eslabQol=true ham qo'shilsa, u auth_date va first_name orasiga tushadi (e harfi). Server boshqa satr hisoblaydi, Telegram esa bu maydonsiz imzolagan. Natija — har doim 401. Shuning uchun kanon marshrutida tgMaydon uni olib tashlaydi.

2-mashq (o'rta): auth_date holati

authDateHolati(authDate, hozirS, maksS) funksiyasini yozing. U "buzuq" (1–12 raqamdan iborat satr emas), "kelajak" (60 soniyadan ko'p oldinda), "eskirgan" (maksS dan eski) yoki "ok" qaytarsin. Bir kunlik chegara bilan oltita qiymatni sinang: 1 soat oldin, 25 soat oldin, 30 soniya keyin, 1 soat keyin, "1.8e9" va bo'sh satr. Ishora: /^\d{1,12}$/.test(…) (RegExp asoslari va metodlari), padEnd — chiroyli ustun uchun.

Yechim
js
function authDateHolati(authDate, hozirS, maksS) {
  if (!/^\d{1,12}$/.test(authDate)) return "buzuq";
  const t = Number(authDate);
  if (t - hozirS > 60) return "kelajak";
  if (hozirS - t > maksS) return "eskirgan";
  return "ok";
}

const hozir = 1_800_000_000;
const KUN = 86_400;
const SINOV = [
  String(hozir - 3_600),
  String(hozir - 90_000),
  String(hozir + 30),
  String(hozir + 3_600),
  "1.8e9",
  "",
];
for (const a of SINOV) {
  console.log(a.padEnd(10), authDateHolati(a, hozir, KUN));
}

Konsolda:

text
1799996400 ok
1799910000 eskirgan
1800000030 ok
1800003600 kelajak
1.8e9      buzuq
           buzuq

"1.8e9" — Number("1.8e9") 1 800 000 000 ga teng, ya'ni "to'g'ri" vaqt! Regex bo'lmasa, bunday g'alati yozuv ham o'tib ketardi. Kanon ham avval regex bilan tekshiradi: kutilgan format — faqat raqamlar. 30 soniya kelajak — ok: kichik soat farqiga ruxsat.

3-mashq (qiyin): vidjetni to'liq tekshirish

Web Crypto bilan widgetTekshir(maydon, token, hozirS, maksS) ni yozing. U "maydon" (hash yoki id yo'q), "imzo", "eskirgan" yoki "ok" qaytarsin. Tartib — kanondagidek: maydon, imzo, muddat. Satrlarni solishtirish — timingSafeEqual o'rniga XOR bilan, oxirigacha. To'rt holatda sinang: to'g'ri, ismi o'zgartirilgan, 25 soat eski, hash siz. Ishora: SHA-256 va HMAC — «Brauzerda sinab ko'ring» bo'limidagi kod; XOR solishtirish — Parolni xavfsiz saqlash darsining 3-mashqida.

Yechim
js
const enc = new TextEncoder();
const hex = (b) => Array.from(new Uint8Array(b),
  (x) => x.toString(16).padStart(2, "0")).join("");

async function hisobla(maydon, token) {
  const satr = Object.keys(maydon).filter((k) => k !== "hash")
    .sort().map((k) => `${k}=${maydon[k]}`).join("\n");
  const sir =
    await crypto.subtle.digest("SHA-256", enc.encode(token));
  const kalit = await crypto.subtle.importKey("raw", sir,
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const imzo =
    await crypto.subtle.sign("HMAC", kalit, enc.encode(satr));
  return hex(imzo);
}

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

async function widgetTekshir(maydon, token, hozirS, maksS) {
  if (!maydon.hash || !maydon.id) return "maydon";
  if (!tengmi(await hisobla(maydon, token), maydon.hash)) {
    return "imzo";
  }
  const t = Number(maydon.auth_date);
  if (!(t > 0) || hozirS - t > maksS || t - hozirS > 60) {
    return "eskirgan";
  }
  return "ok";
}

const TOKEN = "7000000001:SoxtaToken_faqat-dars-uchun-0123456789";
const hozir = 1_800_000_000;
const imzola = async (m) => ({ ...m, hash: await hisobla(m, TOKEN) });

const aziz = await imzola({ id: "42", first_name: "Aziz",
  auth_date: String(hozir - 60) });
console.log(await widgetTekshir(aziz, TOKEN, hozir, 86_400));
console.log(await widgetTekshir({ ...aziz, first_name: "Ali" },
  TOKEN, hozir, 86_400));
const eski =
  await imzola({ id: "42", auth_date: String(hozir - 9e4) });
console.log(await widgetTekshir(eski, TOKEN, hozir, 86_400));
console.log(await widgetTekshir({ id: "42" }, TOKEN, hozir, 86_400));

Konsolda:

text
ok
imzo
eskirgan
maydon
  • Ismni "Ali" ga almashtirish imzoni buzdi — har maydon himoyalangan, faqat id emas.
  • Eski ma'lumot imzosi to'g'ri, lekin muddat o'tgan. Tartib tufayli biz buni aniq bilamiz: imzo o'tgan, keyin muddat yiqilgan.
  • !(t > 0) — NaN ni ham ushlaydi: NaN > 0 doim false.

14. Real ishda

  • Telegram'ning yangi OIDC kirishi. Oldingi darsdagi Oidc klassi standart provayder bilan ishlaydi. Nazariy jihatdan Telegram uchun Map ga issuer: "https://oauth.telegram.org" bilan yana bir qator qo'shiladi. Hujjatga ko'ra id_token'dagi aud — bot ID, imzo sukut bo'yicha RS256 (2026-10 holatiga). Lekin tashqiKirish tasdiqlangan email talab qiladi, Telegram esa email bermaydi — bunday kirish 403 bilan rad bo'lardi. Shuning uchun Telegram OIDC uchun ham "ulash, keyin kirish" siyosati kerak bo'ladi. Kanon buni qurmagan va sinamagan.
  • Telegram botlari — bot ham foydalanuvchini Telegram id bo'yicha taniydi. Saytda ulangan tashqi_hisoblar qatori bot va sayt hisobini bog'laydi: bot "vazifangiz muddati keldi" deb aynan kerakli odamga yozadi.
  • Mini App do'konlari, o'yinlar, xizmatlar — initData tekshiruvi har so'rovda yoki bir marta, keyin o'z sessiyasi (Telegram Mini App backendi va Stars).
  • Intervyuda: "Telegram Login ma'lumotini qanday tekshirasiz?", "Widget va Mini App kalitlari farqi?", "auth_date nima uchun?", "Bot tokeni sizsa nima bo'ladi?".

Xulosa

  • Telegram ma'lumotni bot tokenidan hosil qilingan kalit bilan HMAC-SHA256 qilib imzolaydi; server imzoni qayta hisoblaydi.
  • Tekshiruv satri: hash siz hamma maydon, alifbo tartibida, kalit=qiymat, \n bilan.
  • Vidjet kaliti — SHA256(token), Mini App kaliti — HMAC_SHA256("WebAppData", token); biri ikkinchisiga yaramaydi.
  • auth_date — takroriy hujumga qarshi; kanon 1 kun va +60 s chegarasi bilan, sizda qisqaroq bo'lishi mumkin.
  • Ed25519 signature — tokensiz, uchinchi tomon tekshiruvi; hujjat bo'yicha, kanonda funksiya bor, endpoint yo'q.
  • Telegram email bermaydi: yangi hisob ochilmaydi — ulash, keyin kirish.
  • Telegram'ning hozirgi rasmiy yo'li — OIDC; kanon o'rgatgan HMAC vidjet — eski (legacy) usul, Mini App esa hamon HMAC (OIDC sinalmagan).

Keyingi dars: OneID orqali kirish — O'zbekistonning davlat identifikatsiya tizimi: OAuth oqimi, PINFL va shaxsiy ma'lumotlar talablari.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Telegram Login va Mini App initData: HMAC imzosini serverda tekshirish — IlmHamroh