IlmHamroh
JavaScript Full-stack/30-qism. NestJS59/65-dars20 daqiqa
Mundarija (29)

OAuth NestJS'da: Google bilan kirish, Telegram imzosi, Better Auth va OneID

Qisqacha: Nest'da tashqi kirish ikki yo'l bilan yoziladi. Birinchisi — Passport strategiyasi: passport-google-oauth20 bilan AuthGuard("google") foydalanuvchini Google'ga 302 bilan yo'naltiradi, qaytishda validate(profile) hisobni topadi. Ikkinchisi — tayyor auth kutubxonasi: Better Auth o'z Nest moduli bilan global guard, sessiya va provayderlarni beradi. vazifalar-api 28-qismdagi yo'lda qoladi: Google — openid-client bilan to'g'ridan (OIDC), Telegram — o'z imzo tekshiruvi; endi ular Nest controller'larida. Ko'chishda 28-qismdan qolgan ikki xato tuzatildi: OTP kodlarini hisob bo'yicha cheklash va Google'ning email_verified: "true" satri.

Bu darsda

  • passport-google-oauth20 strategiyasini yozasiz va yo'naltirish manzilidagi har parametrni (state, PKCE, scope) o'qiysiz.
  • Telegram Login imzosini tekshirishni Nest guard'iga qanday o'rashni bilasiz.
  • Better Auth'ning Nest integratsiyasi nima berishini va nima olib qo'yishini baholaysiz.
  • OneID kabi nostandart provayder uchun strategiya qanday quriladi — tushunasiz.
  • vazifalar-api ga auth controller'larini qo'shasiz va ikki ochiq xatoni test bilan tuzatasiz.

Oldin bilishingiz kerak: CASL bilan policy-based avtorizatsiya, OAuth 2.0 va OpenID Connect nazariyasi, Google bilan kirish, Telegram Login va Mini App initData tekshiruvi, OneID orqali kirish, Auth kutubxonalari va xizmatlari.

Oldingi uch darsda "kim?" va "nima qila oladi?" qatlamlari Nest'ga ko'chdi. Endi "qayerdan kirdi?" savoli: parol, SMS, Google, Telegram, passkey. Mantiq 28-qismda yozilgan — bu darsda uni Nest'ning standart usuli bilan solishtiramiz va controller'larga joylaymiz.

1. Nega bu kerak?

Google bilan kirish darsida OIDC oqimini qo'lda yozgandik: openid-client, PKCE, state cookie'si, id_token tekshiruvi. Har qadam ko'z oldimizda edi.

Nest loyihalarida bu oqim ko'pincha bitta strategiya faylida yashaydi: GoogleStrategy extends PassportStrategy(Strategy, "google"). Qadamlar kutubxona ichida. Kod qisqa, lekin nima bo'layotganini bilmasangiz — xato ham ichkarida yashirinadi. Ayniqsa bitta xato: email_verified ni tekshirmasdan hisobni bog'lash. 28-qismdagi pre-hijacking hujumi aynan shundan boshlanadi.

O'xshatish: mehmonxona qabulxonasi. Passport strategiyasi — tayyor "Google pasporti" tekshiruvchisi: pasportni oladi, haqiqiyligini tekshiradi. Lekin "bu odam qaysi xonada yashaydi" degan savolga baribir qabulxona (sizning validate() ingiz) javob beradi. Noto'g'ri xona bersangiz — tekshiruvchining aybi yo'q.

2. passport-google-oauth20

2.1 Strategiya

Demo loyihada passport-google-oauth20@2.0.0 va uning turlarini o'rnatdik. Google'ga haqiqiy so'rov yubormaymiz — faqat Nest qayerga yo'naltirishini ko'ramiz. Client ID va sir — namuna:

ts
@Injectable()
class GoogleStrategy extends PassportStrategy(Strategy, "google") {
  constructor() {
    super({
      clientID: "demo-client-id.apps.googleusercontent.com",
      clientSecret: "demo-sir",
      callbackURL: "http://localhost:3000/auth/google/qaytish",
      scope: ["openid", "email", "profile"],
      state: true, // CSRF himoyasi (sessiya kerak)
      pkce: true,
    });
  }

  // Google kodni tokenga almashtirgach chaqiriladi
  validate(_a: string, _r: string, p: Profile) {
    const email = p.emails?.[0];
    return { googleId: p.id, email: email?.value,
      tasdiqlangan: email?.verified };
  }
}

@Controller("auth/google")
class GoogleController {
  @Get()
  @UseGuards(AuthGuard("google"))
  boshla(): void { /* guard Google'ga 302 qiladi */ }

  @Get("qaytish")
  @UseGuards(AuthGuard("google"))
  qaytish(@Req() req: Request) { return req.user; }
}

Ikki yo'lda bir xil guard turibdi, lekin ishi har xil:

  • /auth/google — so'rovda code yo'q. Guard Google'ga yo'naltiradi, handler umuman ishlamaydi (shuning uchun bo'sh).
  • /auth/google/qaytish — Google code bilan qaytaradi. Guard kodni tokenga almashtiradi, profilni oladi va validate() ni chaqiradi. Natija — req.user.
sequenceDiagram
  participant B as Brauzer
  participant N as Nest (AuthGuard google)
  participant G as Google
  B->>N: GET /auth/google
  N->>B: 302 + state, code_challenge
  B->>G: kirish va rozilik
  G->>B: 302 /qaytish?code&state
  B->>N: GET /auth/google/qaytish
  N->>G: code + code_verifier
  G->>N: tokenlar, profil
  N->>N: validate(profile) → req.user

Diagrammada guard ikki marta ishlaydi: tepada — yo'naltiradi, pastda — kodni almashtiradi. Brauzer Google tokenini hech qachon ko'rmaydi: almashtirish server bilan Google o'rtasida.

validate() ning argumentlari: access token, refresh token va profil. Bizga profil kerak. Email bilan birga verified belgisi ham keladi — undan keyinroq foydalanamiz.

2.2 Biz ishga tushirdik

state va PKCE uchun Passport sessiya so'raydi (u code_verifier va state ni shu yerda eslab qoladi). Demoda har so'rovga bo'sh obyekt qo'ydik — faqat yo'naltirishni ko'rish uchun. Haqiqiy loyihada bu express-session yoki imzolangan cookie bo'ladi. Natija (NestJS 12.2.0, @nestjs/passport 12.0.0):

text
GET /auth/google → 302
Location: https://accounts.google.com/o/oauth2/v2/auth
  response_type = code
  redirect_uri = http://localhost:3000/auth/google/qaytish
  scope = openid email profile
  code_challenge = <43 belgi>
  code_challenge_method = S256
  state = <24 belgi>
  client_id = demo-client-id.apps.googleusercontent.com
GET /auth/google/qaytish?code=soxta&state=x → 401 {"message":"Unauthorized","statusCode":401}

state va code_challenge tasodifiy — uzunligini qoldirdik. Har qatorni OAuth 2.0 nazariyasi darsidan taniysiz:

  • response_type=code — Authorization Code oqimi.
  • code_challenge + S256 — PKCE: code_verifier ning SHA-256 hash'i (43 belgi base64url).
  • state — login CSRF himoyasi: qaytishda xuddi shu qiymat kelishi kerak.

Oxirgi qatorga qarang: soxta state bilan qaytish — 401, Google'ga murojaat ham qilinmadi. Strategiya sessiyadagi state ni topa olmadi va to'xtadi. 28-qismdagi testlar ham shu holatni tekshiradi: "holat cookie'si yo'q, imzosi buzilgan yoki boshqa brauzerniki — oidc-xato".

2.3 validate() ichida nima qilinadi

Demo profilni qaytardi. Haqiqiy validate() — eng mas'uliyatli joy. U uch holatni ajratadi (Google bilan kirish darsidagi qoidalar):

Holat Qaror
Shu sub bilan hisob bor o'sha hisob (email o'zgargan bo'lsa ham)
Shu email bilan tasdiqlangan hisob bor va Google emaili tasdiqlangan bog'lash
Shu email bilan tasdiqlanmagan hisob bor hujumchi oldindan ochgan bo'lishi mumkin: uning paroli o'chiriladi, sessiyalari bekor
Google emaili tasdiqlanmagan rad etish

Hisob sub bo'yicha topiladi, email bo'yicha emas: email o'zgaradi, sub — yo'q. Bu jadval 28-qismdagi tashqiKirish() funksiyasida yozilgan va o'zgarmagan.

Tekshirib ko'ring: Nega validate() hisobni email bo'yicha emas, sub bo'yicha qidiradi?

Javob

Email — o'zgaruvchan va qayta beriladigan narsa. Foydalanuvchi Google'da emailini o'zgartirishi, kompaniya esa eski xodimning manzilini boshqasiga berishi mumkin. sub — provayder ichida bu odamga berilgan doimiy raqam. Email bo'yicha qidirsangiz, emailni olgan begona odam eski egasining hisobiga kiradi.

3. Telegram imzosi — Nest guard'ida

3.1 Tekshiruv

Telegram Login Widget — OAuth emas. Brauzer foydalanuvchi ma'lumotini imzo (hash) bilan to'g'ridan serverga yuboradi. Server imzoni bot tokeni bilan tekshiradi (Telegram Login darsi): sir — SHA256(bot_token), imzo — HMAC-SHA256(sir, tekshiruv_satri). Tekshiruv satri — hash dan boshqa maydonlar, alifbo tartibida, kalit=qiymat, \n bilan.

Web Crypto bilan model (brauzerda ishlaydi). Token — soxta, faqat shu blok uchun:

js
const hex = (b) => [...new Uint8Array(b)]
  .map((x) => x.toString(16).padStart(2, "0")).join("");
const satr = (m) => Object.keys(m).filter((k) => k !== "hash").sort()
  .map((k) => `${k}=${m[k]}`).join("\n");

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

const TOKEN = "123456:SOXTA-token-faqat-darsda";
const m = { id: "777", first_name: "Aziz", auth_date: "1793600000" };
m.hash = await imzo(m, TOKEN);
console.log("to'g'ri:", m.hash === (await imzo(m, TOKEN)));
const soxta = { ...m, id: "1" }; // boshqa odam bo'lib ko'rinish
const qayta = await imzo(soxta, TOKEN);
console.log("id almashtirildi:", soxta.hash === qayta);
console.log("boshqa bot:", m.hash === (await imzo(m, "999:boshqa")));

Konsolda:

text
to'g'ri: true
id almashtirildi: false
boshqa bot: false

Modelda soddalashtirgan joylar: haqiqiy kod auth_date eskirmaganini ham tekshiradi va satrlarni timingSafeEqual bilan solishtiradi (28-qismdagi widgetTekshir).

3.2 Guard sifatida

Nest uslubida bu tekshiruvni guard qilish tabiiy: imzo noto'g'ri bo'lsa, so'rov controller'gacha yetmasin.

ts
@Injectable()
export class TelegramGuard implements CanActivate {
  constructor(@Inject(TELEGRAM) private readonly tg: TgSozlama) {}

  canActivate(ctx: ExecutionContext): boolean {
    const req = ctx.switchToHttp().getRequest<Request>();
    const n = widgetTekshir(req.body as Record<string, string>,
      this.tg.botToken, this.tg.maksS);
    if (n.tur !== "ok") throw new UnauthorizedException(n.sabab);
    req.telegram = n.foydalanuvchi; // keyingi handler uchun
    return true;
  }
}

Bu bizning eskiz (kanon emas). Diqqat: guard pipe'dan oldin ishlaydi (so'rov hayot sikli) — tana hali sxema bilan tekshirilmagan. Shuning uchun widgetTekshir noma'lum shakldagi obyektni xavfsiz o'qishi kerak. Kanon boshqacha qiladi: POST /v1/auth/telegram handler'i tanani avval Zod sxemasidan o'tkazadi, keyin xizmat (telegramKirish) imzoni tekshiradi. Ikkalasi ham to'g'ri, kanonniki tartib jihatidan xavfsizroq.

Ikki yondashuvni so'rov yo'li bo'yicha yonma-yon qo'yamiz:

Qadam Eskiz: TelegramGuard Kanon: handler + xizmat
guard imzo tekshiruvi — tana hali xom KirishGuard: yo'l @Ochiq() — o'tadi
pipe tana sxemasi (agar bo'lsa) Zod sxemasi (TelegramSxemasi): qiymatlar satr yoki son (satrga aylanadi), hash — 128 belgigacha satr, ortiqcha maydon yo'q
handler / xizmat req.telegram ni ishlatadi telegramKirish(): imzo, auth_date, sessiya
xato UnauthorizedException — Nest formati IlovaXatosi — Problem Details

Guard varianti qulayroq ko'rinadi: imzo tekshiruvi bir joyda, handler toza. Lekin u xom tana bilan ishlashga majbur. Masalan, hujumchi hash o'rniga massiv yuborsa, widgetTekshir buni o'zi ushlashi kerak — aks holda 500. Kanon variantida bu savol yo'q: imzo tekshiruviga faqat shakli tasdiqlangan obyekt yetib keladi.

4. Tayyor yechimlar: Better Auth va OneID

4.1 Better Auth Nest'da

Auth kutubxonalari darsida Better Auth'ni o'lchagandik: 30 paket, 20 MB, ichida parol, sessiya, OAuth provayderlari va plaginlar. Uning Nest integratsiyasi — jamiyat paketi @thallesp/nestjs-better-auth, Better Auth hujjatining "NestJS Integration" sahifasida tavsiya qilingan. Hujjat bo'yicha (biz sinamadik, 2026-11-02 da o'qildi):

ts
// main.ts — Better Auth xom tanani o'zi o'qiydi
const app = await NestFactory.create(AppModule, {
  bodyParser: false, // Required for Better Auth
});

// app.module.ts — global AuthGuard: hamma yo'l yopiq
@Module({ imports: [AuthModule.forRoot({ auth })] })
export class AppModule {}

// controller — @Session(), @AllowAnonymous(), @OptionalAuth()
@Get("me")
getProfile(@Session() session: UserSession) {
  return { user: session.user };
}

Taniysizmi? Global guard, "yopiq sukut", ochiq yo'llar uchun dekorator — global guard darsidagi naqshning aynan o'zi, faqat tayyor holda. bodyParser: false ham tanish: vazifalar-api ham Nest'ning parser'ini o'chirgan, chunki o'z express.json limitlari bor.

Nima olasiz va nima berasiz:

Olasiz Berasiz
ro'yxat, login, OAuth, 2FA — tayyor sxema kutubxonaniki (jadvallar, nomlar)
tez boshlash xato javoblari formati kutubxonaniki
plaginlar (tashkilot, passkey) 20 MB va 30 paket, yangilanishlarga bog'liqlik

Yangi loyiha, kichik jamoa, vaqt kam — kuchli tanlov. vazifalar-api da esa auth allaqachon yozilgan, testlangan va Problem Details bilan gaplashadi — almashtirish faqat xavf qo'shadi.

4.2 OneID: nostandart provayder

OneID orqali kirish darsida ko'rdik: OneID OAuth g'oyasida ishlaydi, lekin standart OIDC emas — ochiq discovery yo'q, o'z grant_type qiymatlari, bitta endpoint. Shuning uchun tayyor "OIDC strategiyasi" ishlamaydi.

Passport dunyosida bunday holat uchun umumiy passport-oauth2 strategiyasi bor: manzillar, client ID va userProfile() ni o'zingiz berasiz. Yoki Passport NestJS'da darsidagi kanon tajribasi kabi passport-custom bilan butunlay o'z validate(req) ingiz. Ikkala holatda ham og'ir qism Passport emas, provayder protokolini to'g'ri bajarish — va uni faqat rasmiy hujjat bilan qilish mumkin. Biz OneID'ni sinamadik; parametrlar ulanish shartnomasi bilan beriladi.

4.3 Tanlov: strategiya, kutubxona yoki o'z kod

Endi uch yo'lni bilasiz. Qaysi birini qachon tanlash — savollar bilan:

  • Nechta tashqi provayder kerak? Bitta-ikkita (Google, Telegram) — o'z kod yoki openid-client yetarli. O'nta (Google, GitHub, Apple, Microsoft, Facebook…) — Passport strategiyalari yoki Better Auth vaqtni tejaydi.
  • Auth hali yo'qmi yoki allaqachon bormi? Noldan boshlasangiz — kutubxona tez. Ishlab turgan, testlangan auth'ni almashtirish esa hech qachon "tez" emas: har oqim va har test qayta tekshiriladi.
  • Javob formati va sxema kimniki? API shartnomangiz qat'iy bo'lsa (Problem Details, o'z jadval nomlari) — kutubxona bilan doim "ishqalanish" bo'ladi.
  • Kim javob beradi? Xavfsizlik yangilanishlari: o'z kodingizda — sizda, kutubxonada — kutubxona mualliflarida, lekin yangilash baribir sizning ishingiz.

vazifalar-api uchun javoblar aniq: ikki provayder, auth tayyor va testlangan, shartnoma qat'iy. Shuning uchun kanon o'z kodida qoldi. Boshqa loyihada javoblar boshqacha bo'lishi mumkin — va to'g'ri qaror ham boshqacha bo'ladi.

5. Kanon: auth controller'lari va ikki ochiq xato

5.1 28-qism routeri → Nest controller

28-qismdagi marshrut.ts Nest'da AuthController bo'ldi (326 qator) — biznes qoidalari xizmat.ts da o'zgarmagan. Klass darajasida @Ochiq() (login, ro'yxat ochiq), sessiya kerak bo'lgan metodlarda @SessiyaKerak(). Google qaytishi — kanon faylidan, bo'lingan:

ts
@Get("oidc/:provayder/qaytish")
async oidcQaytish(@Param("provayder") nom: string,
  @Req() req: Request, @Res() res: Response): Promise<void> {
  const p = this.d.oidc.get(nom);
  if (p === undefined) throw new TopilmadiXatosi(
    "Bunday provayder sozlanmagan", "provayder-yoq",
    "Provayder yo'q");
  try {
    const k = await p.yakunla(req, res,
      cookieOl(req.headers.cookie, p.cookieNomi));
    const f = await this.#xizmat.tashqiKirish(k, mijoz(req));
    await this.auth.sessiyaBer(req, res, f, true);
    res.redirect(303, `${this.d.ilovaUrl}/`);
  } catch (x) {
    if (!(x instanceof IlovaXatosi)) throw x;
    res.redirect(303, `${this.d.ilovaUrl}/kirish?xato=${
      encodeURIComponent(x.kod ?? "xato")}`);
  }
}

Bu yerda @Res() — passthrough siz. Brauzer navigatsiyasi JSON emas, yo'naltirish kutadi; javobni metod o'zi yuboradi (Controller'lar). Xato bo'lsa ham foydalanuvchi bo'sh sahifa emas, frontend'dagi "kirish" sahifasini ko'radi, xato kodi query'da.

Sinov — haqiqiy Google emas, testning ichida ko'tariladigan lokal oidc-provider 9.12.2 (soxta provayder, haqiqiy OIDC protokoli). Biz testlarni o'z PostgreSQL 17.6 klasterimizda yurgizdik — 28-qismdagi 6 ta va yangi 1 ta, hammasi o'tdi.

5.2 Ochiq xato 3: email_verified satr bo'lib keladi

OIDC Core spetsifikatsiyasi email_verified ni boolean deydi. Amalda ba'zi provayderlar (eski Google userinfo, Cognito, ba'zi Azure AD sozlamalari — kanon izohi bo'yicha) uni "true" satri qilib beradi. 28-qismdagi kod faqat === true ni qabul qilardi — haqiqiy foydalanuvchi "email tasdiqlanmagan" deb rad etilardi.

Tuzatish — aniq ikki qiymat, qolgani rad (kanon faylidan, bo'lingan):

diff
-    emailTasdiqlangan: u["email_verified"] === true,
+    // faqat aniq true / "true" — tasdiqlangan; "false", "1", "yes"
+    // va boshqasi — YO'Q (xavfsiz tomonga)
+    const tasdiq = u["email_verified"];
+    … emailTasdiqlangan: tasdiq === true || tasdiq === "true", …

Paketsiz model — nima o'tadi, nima yo'q:

js
const tasdiqlanganmi = (v) => v === true || v === "true";
for (const v of [true, "true", false, "false", "1", "yes", 1]) {
  console.log(JSON.stringify(v), "→", tasdiqlanganmi(v));
}

Konsolda:

text
true → true
"true" → true
false → false
"false" → false
"1" → false
"yes" → false
1 → false

Boolean("false") — true! Shuning uchun "satrni boolean'ga aylantirish" xavfli: "false" satri tasdiqlangan bo'lib ketardi. Biz yurgizgan test (lokal provayder satr qaytaradi):

text
✓ test/oidc.test.ts > email_verified="true" (satr) — tasdiqlangan deb bog'lanadi; "false" (satr) — rad 3808ms

5.3 Ochiq xato 2: OTP — kod emas, hisob bo'yicha chegara

28-qismdagi SMS kirishda bitta kodga 5 urinish berilardi. Lekin hujumchi kuniga 10 ta yangi kod so'rashi mumkin — 50 urinish. Kanon hisobi: oyiga ~1 500 urinish, 6 xonali kodda bu ~0.15 % ehtimol. NIST SP 800-63B (§5.2.2) muvaffaqiyatsiz urinishlarni hisob bo'yicha cheklashni talab qiladi, 100 tadan oshmasin.

Tuzatish: oxirgi muvaffaqiyatli OTP'dan beri hisobning ketma-ket xatolari ≤ 100. Hisoblash uchun yangi jadval kerak bo'lmadi — otp_kodlar dagi urinishlar yig'indisi; parallel urinishlar chegaradan o'tib ketmasligi uchun foydalanuvchi qatori FOR UPDATE bilan qulflanadi (qulflar).

Tuzatish paytida yana bir xato topildi. Yangi SMS so'ralganda eski kodlar "bekor" qilinardi — ishlatilgan_vaqt = now() bilan. Lekin "oxirgi muvaffaqiyatli OTP" aynan ishlatilgan_vaqt bo'yicha topiladi. Ya'ni har yangi SMS hisoblagichni nollardi — hujumchi har yangi kod bilan hisobni "tozalardi". Endi bekor qilish tugash_vaqti = now() bilan, ishlatilgan_vaqt esa faqat haqiqiy muvaffaqiyatda:

diff
-      UPDATE otp_kodlar SET ishlatilgan_vaqt = now()
-        WHERE telefon = $1 AND ishlatilgan_vaqt IS NULL
+      UPDATE otp_kodlar SET tugash_vaqti = now()
+        WHERE telefon = $1 AND ishlatilgan_vaqt IS NULL
+          AND tugash_vaqti > now()

Tashqi javob o'zgarmadi — chegaradan keyin ham oddiy 401 ("kod noto'g'ri"), hujumchi chegara borligini bilmaydi. Jurnalda esa sabab ko'rinadi: otp-hisob-chegarasi. Biz yurgizgan test:

text
✓ test/otp.test.ts > hisob chegarasi: 100 ketma-ket xatodan keyin TO'G'RI kod ham 401 (jurnalda otp-hisob-chegarasi); 99 da — 200 va hisoblagich nollanadi 1134ms

Test nomini o'qing: ikkita holat bor. 100 xatodan keyin to'g'ri kod ham rad etiladi — chegara ishladi. 99 xatoda esa to'g'ri kod 200 oladi va hisoblagich nolga tushadi — haqiqiy foydalanuvchi jazolanmaydi.

Chegaradan keyin hisob yo'qolmaydi: parol, passkey yoki parolni tiklash orqali kirish ochiq qoladi.

Tekshirib ko'ring: Chegaradan keyin nega 429 yoki "hisob bloklandi" emas, oddiy 401 qaytadi?

Javob

429 yoki maxsus xabar hujumchiga "chegaraga yetding" deb aytadi. U shundan hisoblagich qachon nollanishini taxmin qiladi yoki boshqa hisobga o'tadi. Oddiy 401 esa "kod noto'g'ri" bilan bir xil ko'rinadi: hujumchi to'g'ri kodni topsa ham, buni bilmaydi. Haqiqiy foydalanuvchi uchun boshqa yo'llar (parol, passkey, tiklash) ochiq, sabab esa jurnalda — administrator ko'radi.

6. Hujumchi nigohi

  1. Tasdiqlanmagan email bilan bog'lash. Hujumchi sizning emailingiz bilan hisob ochadi, siz keyin Google bilan kirasiz — hisob "bog'lanadi", hujumchining paroli esa ishlayveradi (pre-hijacking). Himoya — validate() dagi jadval.
  2. state siz OAuth. Hujumchi o'z code ini sizning brauzeringizga tiqadi — siz uning hisobiga kirasiz (login CSRF). Biz ko'rdik: soxta state — 401.
  3. Satrni boolean'ga aylantirish. Boolean("false") === true. Faqat aniq qiymatlar.
  4. OTP'ni kod bo'yicha cheklash. Yangi SMS — yangi 5 urinish. Chegara hisobda bo'lsin, hisoblagichni nollaydigan har yo'lni tekshiring.
  5. Guard'da tekshirilmagan tana. Guard pipe'dan oldin — tanadagi hash massiv yoki obyekt bo'lishi mumkin. Avval shaklni tekshiring.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
redirect_uri_mismatch (Google sahifasida) callbackURL konsoldagi bilan aynan bir xil emas port, yo'l, http/https — belgima-belgi
Qaytishda doim 401 state: true, lekin sessiya yo'q express-session yoki o'z holat cookie'si
Profilda email yo'q scope da email yo'q scope: ["openid", "email", "profile"]
Better Auth yo'llari tanani ko'rmaydi Nest body parser yoqiq bodyParser: false
Telegram imzosi doim noto'g'ri maydonlar alifbo tartibida emas yoki hash satrga qo'shilgan tartiblang, hash ni chiqarib tashlang

8. Mashqlar

Mashqlar kurs/mashqlar/30/59-oauth/ da.

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

  • Google strategiyasi paketi — passport-
  • Login CSRF'dan himoya qiluvchi parametr —
  • Hisob doim shu maydon bo'yicha topiladi —
  • Better Auth Nest'da o'chirishni so'raydigan sozlama — : false
  • OTP chegarasi kanonda nima bo'yicha —
Yechim
  • google-oauth20 — Passport strategiyasi; Nest'da PassportStrategy(Strategy, "google").
  • state — tasodifiy qiymat; qaytishda mos kelmasa — 401.
  • sub — provayder ichidagi doimiy identifikator; email o'zgaradi.
  • bodyParser — kutubxona xom tanani o'zi o'qiydi.
  • hisob — kod bo'yicha emas; NIST SP 800-63B, 100 tagacha xato.

2-mashq (o'rta): bog'lash qarori

qaror(google, mavjud) funksiyasini yozing. google — { sub, email, emailVerified }, mavjud — bazadagi hisoblar massivi { id, sub?, email, tasdiqlangan }. Natija: "kirish:<id>" (sub bo'yicha topildi), "bog'lash:<id>" (tasdiqlangan email), "egallash:<id>" (tasdiqlanmagan hisob — hujumchi paroli o'chadi), "yangi" yoki "rad" (Google emaili tasdiqlanmagan). Ishora: find va darsdagi jadval tartibi.

Yechim
js
const tasdiqlanganmi = (v) => v === true || v === "true";

function qaror(google, mavjud) {
  const bySub = mavjud.find((h) => h.sub === google.sub);
  if (bySub) return `kirish:${bySub.id}`;
  if (!tasdiqlanganmi(google.emailVerified)) return "rad";
  const byEmail = mavjud.find((h) => h.email === google.email);
  if (!byEmail) return "yangi";
  return byEmail.tasdiqlangan
    ? `bog'lash:${byEmail.id}` : `egallash:${byEmail.id}`;
}

const baza = [
  { id: 1, sub: "g-1001", email: "aziz@example.com",
    tasdiqlangan: true },
  { id: 2, email: "dilnoza@example.com", tasdiqlangan: true },
  { id: 3, email: "qurbon@example.com", tasdiqlangan: false },
];
const g = (sub, email, emailVerified) =>
  ({ sub, email, emailVerified });
console.log(qaror(g("g-1001", "yangi@example.com", true), baza));
console.log(qaror(g("g-2002", "dilnoza@example.com", "true"), baza));
console.log(qaror(g("g-3003", "qurbon@example.com", true), baza));
console.log(qaror(g("g-4004", "bobur@example.com", "false"), baza));
console.log(qaror(g("g-5005", "bobur@example.com", true), baza));

Konsolda:

text
kirish:1
bog'lash:2
egallash:3
rad
yangi

Birinchi qatorda email boshqa, lekin sub mos — o'sha Aziz. Uchinchi qator — pre-hijacking himoyasi: tasdiqlanmagan hisob Google egasiga o'tadi, avvalgi parol o'chiriladi. To'rtinchisi — "false" satri rad etildi.

3-mashq (qiyin): hisoblagichni nollaydigan xato

OTP hisob chegarasining modelini yozing. Kodlar — massiv { urinish, ishlatildi, bekor }. xatolar() — oxirgi ishlatildi koddan keyingi kodlarning urinishlari yig'indisi. Ikki variantdagi yangiKod(bekorUsuli) ni yozing: "ishlatildi" (28-qismdagi xato) va "bekor" (tuzatish) — eski ishlatilmagan kodlarni shu usulda yopsin. Hujumchi 3 marta: 5 xato urinish + yangi kod. Har variantda xatolar() necha bo'ladi? Ishora: findLastIndex, slice, reduce.

Yechim
js
function hisob(bekorUsuli) {
  const kodlar = [{ urinish: 0, ishlatildi: false, bekor: false }];
  const xatolar = () => {
    const i = kodlar.findLastIndex((k) => k.ishlatildi);
    return kodlar.slice(i + 1).filter((k) => !k.ishlatildi)
      .reduce((s, k) => s + k.urinish, 0);
  };
  const yangiKod = () => {
    for (const k of kodlar) {
      if (!k.ishlatildi && !k.bekor) k[bekorUsuli] = true;
    }
    kodlar.push({ urinish: 0, ishlatildi: false, bekor: false });
  };
  for (let n = 0; n < 3; n++) {
    kodlar.at(-1).urinish += 5; // 5 ta noto'g'ri kod
    yangiKod();
  }
  return xatolar();
}

console.log("28-qism (ishlatildi bilan bekor):", hisob("ishlatildi"));
console.log("tuzatish (bekor bilan):", hisob("bekor"));

Konsolda:

text
28-qism (ishlatildi bilan bekor): 0
tuzatish (bekor bilan): 15

Xato variantda 15 ta xato urinish izsiz yo'qoldi: har yangi kod eski kodlarni "ishlatilgan" deb belgiladi va hisoblagich noldan boshlandi. Hujumchi cheksiz davom eta olardi — chegara 100 bo'lsa ham. Tuzatishda hamma xato hisobda qoladi. Kanonda ham aynan shu bitta ustun (tugash_vaqti va ishlatilgan_vaqt) farqi hal qildi.

9. Loyiha qadami: auth controller'lari

9.1 Fayllar

Fayl Nima
src/auth/auth.controller.ts yangi: /v1/auth — ro'yxat, email tasdiq, parol tiklash, kirish, chiqish, OTP, telefon, OIDC, Telegram, API kalitlar, sessiyalar
src/auth/mfa.controller.ts yangi: 2FA (TOTP, zaxira kodlar) va passkey yo'llari
src/auth/ichki.controller.ts yangi: xizmatlararo HMAC yo'li + ServisImzoGuard
src/auth/otp.ts, xizmat.ts ochiq xato 2: hisob chegarasi, bekor qilish tugash_vaqti bilan, jurnal sababi
src/auth/oidc.ts ochiq xato 3: email_verified — true yoki "true"

Controller'lar 28-qism routerlarining yo'llari, statuslari va javoblarini aynan saqlaydi — 28-qismdagi testlar ularni o'zgarishsiz tekshiradi. Har metodda tezlik cheklovi dekoratori (@Cheklov(...)) va bot yo'llarida @CaptchaKerak() bor — ular keyingi darsda yoziladigan cheklov.guard.ts dan.

ichki.controller.ts — @Ochiq(), chunki u foydalanuvchi emas, boshqa xizmat uchun. Uni ServisImzoGuard himoya qiladi: 28-qismdagi HMAC middleware'i guard ichiga o'ralgan (vaqt oynasi, nonce takror himoyasi, xom tana baytlari ustida imzo). Biz yurgizgan test: imzosiz — 401, to'g'ri imzo — 200, o'sha nonce takror — 401.

9.2 Tekshiruv: o'n bir kutilgan xato

Shu kesimni biz kanon fayllaridan yig'ib tekshirdik (npx tsc --noEmit). Yangi xatolar:

text
src/auth/auth.controller.ts(19,39): error TS2307: Cannot find module './cheklov.guard.ts' or its corresponding type declarations.
src/auth/mfa.controller.ts(17,25): error TS2307: Cannot find module './cheklov.guard.ts' or its corresponding type declarations.
src/auth/mfa.controller.ts(78,57): error TS7006: Parameter 'req' implicitly has an 'any' type.
src/auth/mfa.controller.ts(78,62): error TS7006: Parameter 'a' implicitly has an 'any' type.
src/auth/mfa.controller.ts(148,67): error TS7006: Parameter 'req' implicitly has an 'any' type.

Qolgan oltitasi — o'tgan darsdagilarning aynan o'zi (qoriqchi.ts ikkitasi, ilova.ts bittasi, eski testlar uchtasi). Yangi beshtasining sababi bitta: cheklov.guard.ts hali yo'q. TS7006 lar — @Cheklov({ kalit: (req, a) => … }) ichidagi parametrlar: tur NestQoida dan kelishi kerak edi. Keyingi darsda bu fayl qo'shilgach, hammasi birdan yo'qoladi.

otp.ts va oidc.ts dagi o'zgarishlar yangi xato bermadi — ular o'z turlari ichida. Hali commit qilmang.

10. Real ishda

  • "Google bilan kirish" — deyarli har Nest loyihasida passport-google-oauth20. Review'da birinchi savol: "validate() email tasdiqlanganini tekshiryaptimi va hisobni nima bo'yicha topyapti?".
  • Telegram — O'zbekistonda eng ko'p so'raladigan kirish usullaridan biri: Login Widget va Mini App initData. Imzo tekshiruvi — serverda, har doim.
  • Better Auth, Clerk, Auth0 — startaplar va prototiplarda tez-tez; katta tizimlar ko'pincha Keycloak yoki o'z auth'iga o'tadi.
  • Intervyu: "OAuth'da state nima uchun?", "PKCE nima?", "Pre-hijacking hujumi qanday ishlaydi?", "OTP bruteforce'dan qanday himoyalanasiz?".

Xulosa

  • passport-google-oauth20: bir guard ikki ish — yo'naltirish (302 + state + PKCE) va qaytish (validate(profile)); soxta state — 401.
  • validate() — hisobni sub bo'yicha topadi, tasdiqlanmagan email bilan bog'lamaydi.
  • Telegram imzosi — HMAC-SHA256; guard yoki xizmatda, lekin shakl tekshiruvidan keyin.
  • Better Auth Nest moduli — tayyor global guard va sessiya; narxi — sxema, format va 20 MB.
  • Kanon: OIDC va Telegram 28-qism mantig'i bilan Nest controller'larida; ochiq xato 3 ("true" satri) va ochiq xato 2 (hisob chegarasi + hisoblagichni nollagan xato) — test bilan tuzatildi.

Keyingi dars: @nestjs/throttler — Nest'ning tezlik cheklovchisini to'liq o'rganamiz, kanon limiteri bilan o'lchaymiz va login lockout DoS'ini CAPTCHA bilan tuzatamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
OAuth NestJS'da: Google bilan kirish, Telegram imzosi, Better Auth va OneID — IlmHamroh