Integratsiya
Bosqichma-bosqich: authorize, callback, token almashtirish, ID tokenni tekshirish, userinfo, refresh, chiqish va kod namunalari.
1. Foydalanuvchini yo'naltirish
Har bir kirish urinishi uchun serveringizda uchta tasodifiy qiymat yarating va foydalanuvchi sessiyasida (yoki qisqa muddatli httpOnly cookie'da) saqlang:
| Qiymat | Qanday yaratiladi | Nima uchun |
|---|---|---|
| state | ≥ 32 bayt tasodifiy, base64url | CSRF va javobni so'rovga bog'lash |
| nonce | ≥ 32 bayt tasodifiy | ID token qayta ishlatilishining oldini olish |
| code_verifier | 32 bayt tasodifiy → base64url (43 belgi) | PKCE — kod o'g'irlansa ham almashtirib bo'lmaydi |
code_challenge = BASE64URL(SHA256(code_verifier)). Faqat S256 qabul qilinadi; plain rad etiladi.
GET https://kitobchi.com/api/id/authorize ?response_type=code &client_id=kid_xxxxxxxxxxxxxxxxxxxxxxxx &redirect_uri=https%3A%2F%2Fhamkor.uz%2Fauth%2Fkitobchi%2Fcallback &scope=openid%20profile%20phone &state=Q2xpZW50U3RhdGVSYW5kb20xMjM0NTY3ODkw &nonce=Tm9uY2VSYW5kb21WYWx1ZTEyMzQ1Njc4OTA &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256
| Parametr | Tavsif | Talab |
|---|---|---|
| response_type | Faqat code | majburiy |
| client_id | Sizga berilgan identifikator | majburiy |
| redirect_uri | Ro'yxatdagilardan biri, belgi-belgi bir xil | majburiy |
| scope | Bo'sh joy bilan ajratilgan, openid bo'lishi shart | majburiy |
| state | 1–512 belgi | majburiy |
| nonce | 1–255 belgi, ID token ichida qaytadi | majburiy |
| code_challenge, code_challenge_method | PKCE, S256 | majburiy |
| prompt | none (jim tekshiruv), login (qayta kirish), consent (rozilikni qayta so'rash), select_account | ixtiyoriy |
| max_age | Soniyalarda; oxirgi kirish bundan eski bo'lsa, qayta SMS so'raladi | ixtiyoriy |
| login_hint | Telefon raqam (998901234567) — maydon oldindan to'ldiriladi | ixtiyoriy |
2. Callback'ni qabul qilish
https://hamkor.uz/auth/kitobchi/callback?code=SplxlOBeZQQYbYS6WxSbIA...&state=Q2xpZW50...&iss=https%3A%2F%2Fkitobchi.com%2Fapi%2Fid
https://hamkor.uz/auth/kitobchi/callback?error=access_denied&error_description=...&state=Q2xpZW50...&iss=...
- state sessiyadagi qiymatga teng bo'lmasa — so'rovni to'xtating. Tekshirgandan keyin sessiyadan o'chiring (bir martalik).
- iss = https://kitobchi.com/api/id ekanini tekshiring (RFC 9207 — bir nechta provayder ulangan bo'lsa «mix-up» hujumidan himoya).
- Kodni darhol almashtiring — 5 daqiqa amal qiladi. Callback sahifasida tashqi skript/analitika bo'lmasin, kod URL'dan Referer orqali sizib chiqmasin.
3. Kodni tokenlarga almashtirish
So'rov application/x-www-form-urlencoded bo'lishi shart (JSON qabul qilinmaydi). Server ilovalar client_secret_basic (tavsiya) yoki client_secret_post; public ilovalar faqat client_id + PKCE.
curl -X POST 'https://kitobchi.com/api/id/token' \ -u 'kid_xxxxxxxxxxxxxxxxxxxxxxxx:kid_cs_xxxxxxxx' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'code=SplxlOBeZQQYbYS6WxSbIA...' \ --data-urlencode 'redirect_uri=https://hamkor.uz/auth/kitobchi/callback' \ --data-urlencode 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
{
"access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
"token_type": "Bearer",
"expires_in": 600,
"scope": "openid profile phone offline_access",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...",
"refresh_token": "kid_rt_Wm9yQmlyb3JSYW5kb21Ub2tlbi4uLg"
}
refresh_token faqat offline_access so'ralganda (va ilovaga ruxsat berilganda) qaytadi.
4. ID tokenni tekshirish
ID token — RS256 bilan imzolangan JWT. Uni tayyor kutubxona bilan tekshiring; quyidagilarning barchasi bajarilishi shart:
- Imzo — JWKS'dagi kid mos kalit bilan; alg faqat RS256 (none/HS256 ni rad eting).
- iss = https://kitobchi.com/api/id
- aud = sizning client_id, azp ham shunga teng.
- exp kelajakda (soat farqi uchun ≤ 60 s leeway), iat juda eski emas.
- nonce sessiyadagi qiymatga teng.
- Kerak bo'lsa auth_time ni max_age bilan solishtiring.
{
"iss": "https://kitobchi.com/api/id",
"sub": "f3Qx0rH9K1c4b7mZpLw2aVd8Yt6NsE5uRj0oGiBkCqA",
"aud": "kid_xxxxxxxxxxxxxxxxxxxxxxxx",
"azp": "kid_xxxxxxxxxxxxxxxxxxxxxxxx",
"iat": 1792526400,
"exp": 1792530000,
"auth_time": 1792526380,
"nonce": "Tm9uY2VSYW5kb21WYWx1ZTEyMzQ1Njc4OTA",
"at_hash": "77QmUPtjPfzWtF2AnpK9RQ",
"amr": ["otp"],
"name": "Aziz Karimov",
"given_name": "Aziz",
"family_name": "Karimov",
"picture": "https://kitobchi.com/storage/avatar/....jpg",
"phone_number": "+998901234567",
"phone_number_verified": true
}
5. UserInfo
curl 'https://kitobchi.com/api/id/userinfo' -H 'Authorization: Bearer eyJ0eXAiOiJhdCtqd3Qi...'
{
"sub": "f3Qx0rH9K1c4b7mZpLw2aVd8Yt6NsE5uRj0oGiBkCqA",
"name": "Aziz Karimov",
"phone_number": "+998901234567",
"phone_number_verified": true
}
Javobdagi sub ID tokendagi bilan bir xil bo'lishi shart — farq qilsa, javobni rad eting. Token yaroqsiz bo'lsa 401 va WWW-Authenticate: Bearer error="invalid_token".
6. Refresh token (aylanuvchi)
curl -X POST 'https://kitobchi.com/api/id/token' \ -u 'kid_xxxxxxxxxxxxxxxxxxxxxxxx:kid_cs_xxxxxxxx' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=refresh_token' \ --data-urlencode 'refresh_token=kid_rt_Wm9yQmlyb3JSYW5kb21Ub2tlbi4uLg'
- Har safar yangi refresh token qaytadi — eskisini darhol yangisi bilan almashtiring (atomik). Eski token ikkinchi marta yuborilsa, bu o'g'irlik belgisi deb hisoblanadi va shu sessiyadagi barcha tokenlar bekor qilinadi; foydalanuvchi qayta kirishi kerak bo'ladi.
- Muddat: oxirgi foydalanishdan 30 kun (sirpanuvchi), birinchi kirishdan maksimal 90 kun. Keyin qayta kirish talab qilinadi.
- Parallel so'rovlar: bir nechta oqim bir vaqtda refresh qilmasin — bitta mutex/lock ostida bajaring.
- Scope'ni toraytirish mumkin (scope=openid), kengaytirish mumkin emas (invalid_scope).
7. Chiqish va bekor qilish
Foydalanuvchi ilovangizdan chiqsa, refresh tokenni bekor qiling (javob doim 200):
curl -X POST 'https://kitobchi.com/api/id/revoke' \ -u 'kid_xxxxxxxxxxxxxxxxxxxxxxxx:kid_cs_xxxxxxxx' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=kid_rt_Wm9yQmlyb3JSYW5kb21Ub2tlbi4uLg' \ --data-urlencode 'token_type_hint=refresh_token'
Kitobchi ID sessiyasini ham yopish kerak bo'lsa (umumiy kompyuterlar), brauzerni yo'naltiring. post_logout_redirect_uri faqat ro'yxatdan o'tgan va faqat imzosi to'g'ri id_token_hint bilan ishlaydi:
GET https://kitobchi.com/api/id/logout?id_token_hint=eyJ...&post_logout_redirect_uri=https%3A%2F%2Fhamkor.uz%2F&state=xyz
Ekotizim ilovalari: brauzersiz kirish
Kitobchi ekotizimiga kiruvchi (first-party) mobil ilovalar foydalanuvchini brauzerga chiqarmasdan, o'z ekranida kirita oladi: ilova raqam va kodni o'zi so'raydi, autentifikatsiyani esa Kitobchi ID bajaradi. Bu imkoniyat faqat Kitobchi tomonidan first_party deb belgilangan ilovalarga ochiq; hamkor (uchinchi tomon) ilovalar yuqoridagi brauzer oqimidan foydalanadi — foydalanuvchi Kitobchi sahifasi va rozilik oynasini ko'rishi shart.
Kod so'rash
Ilova PKCE juftligi va nonce yaratib, raqamni yuboradi. Raqam Kitobchi'da mavjud va foydalanuvchining Kitobchi ilovasi faol bo'lsa, kod Kitobchi ilovasidagi rasmiy «Kitobchi» chatiga (push bilan) yuboriladi — SMS ketmaydi. Aks holda SMS yuboriladi.
SMS zaxirasi
channel = app bo'lsa, ekranda «Kod Kitobchi ilovasiga yuborildi» deb yozing va sms_available_in soniyadan keyin «SMS orqali olish» tugmasini faollashtiring — u o'sha kodni SMS bilan yuboradi.
Kodni tasdiqlash va tokenlar
Kod to'g'ri bo'lsa authorization_code qaytadi — uni odatdagidek /token ga code_verifier bilan almashtirasiz.
curl -X POST 'https://kitobchi.com/api/id/native/start' -H 'Content-Type: application/json' -d '{
"client_id": "kid_xxxxxxxxxxxxxxxxxxxxxxxx",
"phone": "+998901234567",
"scope": "openid profile phone offline_access",
"redirect_uri": "uz.kitobchi.express:/oauth",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"nonce": "Tm9uY2VSYW5kb21WYWx1ZQ"
}'
// 200
{ "auth_session": "q8Xw...", "masked_phone": "+998 90 *** ** 67",
"expires_in": 300, "channel": "app", "sms_available_in": 30 }
{ "client_id": "kid_…", "auth_session": "q8Xw..." }
// 200 → { "channel": "sms" } | 30 s o'tmagan bo'lsa → { "channel": "app", "retry_after": 12 }
{ "client_id": "kid_…", "auth_session": "q8Xw...", "code": "482913" }
// 200 → { "authorization_code": "SplxlOBe...", "redirect_uri": "uz.kitobchi.express:/oauth", "iss": "https://kitobchi.com/api/id" }
// keyin: POST /token grant_type=authorization_code, code, redirect_uri, code_verifier, client_id
| error | HTTP | Ma'nosi |
|---|---|---|
| unauthorized_client | 403 | Ilova first-party emas — brauzer oqimidan foydalaning |
| invalid_phone | 400 | Raqam formati noto'g'ri |
| invalid_code | 400 | Kod noto'g'ri (5 urinishgacha) |
| too_many_attempts, expired_code, invalid_session | 429 / 400 | Jarayonni qaytadan boshlang (/native/start) |
| daily_limit | 429 | Bu raqamga bugun juda ko'p kod yuborilgan |
| delivery_failed | 503 | Kod yuborilmadi, birozdan keyin urinib ko'ring |
Kod namunalari
Node.js (openid-client v6)
import * as client from 'openid-client'
const config = await client.discovery(
new URL('https://kitobchi.com/api/id'),
process.env.KITOBCHI_CLIENT_ID,
process.env.KITOBCHI_CLIENT_SECRET,
)
// /login
const code_verifier = client.randomPKCECodeVerifier()
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier)
const state = client.randomState()
const nonce = client.randomNonce()
req.session.kid = { code_verifier, state, nonce }
res.redirect(client.buildAuthorizationUrl(config, {
redirect_uri: 'https://hamkor.uz/auth/kitobchi/callback',
scope: 'openid profile phone',
code_challenge, code_challenge_method: 'S256', state, nonce,
}).href)
// /auth/kitobchi/callback
const { code_verifier, state, nonce } = req.session.kid
delete req.session.kid
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: code_verifier, expectedState: state, expectedNonce: nonce,
}) // iss, state, imzo, aud, exp, nonce — kutubxona tekshiradi
const claims = tokens.claims() // claims.sub — foydalanuvchi kaliti
PHP (Laravel, firebase/php-jwt)
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Http;
$iss = 'https://kitobchi.com/api/id';
abort_unless(hash_equals(session()->pull('kid_state', ''), (string) $request->query('state')), 403);
abort_unless($request->query('iss') === $iss, 403);
$tokens = Http::asForm()
->withBasicAuth(config('services.kitobchi.client_id'), config('services.kitobchi.secret'))
->post("$iss/token", [
'grant_type' => 'authorization_code',
'code' => $request->query('code'),
'redirect_uri' => route('kitobchi.callback'),
'code_verifier' => session()->pull('kid_verifier'),
])->throw()->json();
$jwks = Cache::remember('kid_jwks', 900, fn () => Http::get("$iss/jwks")->throw()->json());
JWT::$leeway = 60;
$claims = (array) JWT::decode($tokens['id_token'], JWK::parseKeySet($jwks, 'RS256'));
abort_unless($claims['iss'] === $iss && $claims['aud'] === config('services.kitobchi.client_id'), 403);
abort_unless(hash_equals(session()->pull('kid_nonce', ''), $claims['nonce'] ?? ''), 403);
$user = User::firstOrCreate(['kitobchi_sub' => $claims['sub']], [/* ... */]);
Mobil ilovalar (Android / iOS / Flutter)
AppAuth kutubxonasidan foydalaning (Flutter: flutter_appauth). U discovery, PKCE va tizim brauzerini o'zi boshqaradi. Mobil ilova — public client: ilovaga client_secret joylashtirmang. WebView ichida kirish sahifasini ochish taqiqlanadi (foydalanuvchi manzil satrini ko'rmaydi, fishingga yo'l ochiladi).
final result = await const FlutterAppAuth().authorizeAndExchangeCode(
AuthorizationTokenRequest(
'kid_xxxxxxxxxxxxxxxxxxxxxxxx',
'uz.hamkor.app:/oauth',
issuer: 'https://kitobchi.com/api/id',
scopes: ['openid', 'profile', 'phone', 'offline_access'],
),
);
// result.idToken, result.accessToken, result.refreshToken
// refresh tokenni flutter_secure_storage'da saqlang