Asosiy kontentga o‘tish

Maqolalar 24 daqiqa 240 ko‘rish

Google Sign-In: iOS va Android id_token'ini backend'da to'g'ri verify qilish

Mobil ilova «Google bilan kirish»dan id_token oladi — backend uni verify qilmasdan ishonsa, autentifikatsiya teshik bo'ladi. Signature va audience tekshiruvi, ikki platformaning ikki client ID muammosi, public key'larni cache qilish, sub bo'yicha identity — to'liq kod bilan.

mobile security api django

Mundarija

Metro ilovasida ro'yxatdan o'tish bitta tugma: "Google bilan kirish". Foydalanuvchi uchun qulay, backend uchun esa nozik joy: mobil ilova bizga Google'dan olgan id_tokenni yuboradi va biz shu satrga qarab odamga kabinet ochamiz. Token'ni verify qilmasak — istalgan odam istalgan email nomidan kiradi.

Oqim shunday:

Sxema
Mobil ilova ──(Google SDK)──► Google ──► id_token (JWT)
     │
     └──POST /api/auth/google/ {"id_token": "..."}──► bizning backend
                                                          │
                          1) signature — Google'ning public key'i bilan
                          2) audience — token AYNAN BIZNING client ID uchunmi
                          3) iss / exp / iat — kim, qachongacha
                          4) sub bo'yicha identity: topish yoki yaratish
                          5) o'zimizning access/refresh token'ni qaytarish

Besh qadamning uchtasi kutubxona zimmasida, ikkitasi bizniki. Amalda eng ko'p o'tkazib yuboriladigani — ikkinchisi: signature to'g'ri chiqqani token aynan sizning ilovangizga atalganini anglatmaydi. Quyida shu qadam bitta argument tufayli qanday jimgina o'chib qolishini ko'rasiz.

Eng xavfli xato: token'ni verify emas, decode qilish

id_token — oddiy JWT, uni istalgan kutubxona signature'siz ham o'qib beradi (jwt.decode(..., options={"verify_signature": False})). Ichidan chiroyli email chiqadi — va tekshirmasdan ishongan backend'ga istalgan qalbaki token o'tadi. decode — konvertni ochish, verify — muhrni tekshirish. JWT tuzilishini eslash uchun: JWT autentifikatsiya.

Nega o'z access token'imizni beramiz#

Google'ning id_tokeni — identityni isbotlaydigan hujjat, API'ga kirish chiptasi emas: exp ~1 soat, uni yangilash Google SDK'ga bog'liq, ichida bizning rollarimiz va ID'larimiz yo'q. Shuning uchun sxema ikki bosqichli: id_tokenni bir marta verify qilamiz, keyin foydalanuvchiga o'zimizning access/refresh juftligimizni beramiz — API hayoti oddiy JWT autentifikatsiya bilan davom etadi. Mobil ilova ham yengil nafas oladi: har so'rovdan oldin Google SDK'ni qo'zg'atmaydi.

1-qadam: ikki platforma, ikki client ID#

Google Cloud Console'da OAuth client'lar yaratiladi: Android, iOS va backend uchun Web application turidagi client. Backend uchun muhim savol — token ichidagi aud claim'ida ulardan qaysi biri turadi. Bu platformaga bog'liq:

  • Android — ilova Google'dan token so'raganda (setServerClientId) Web client IDni beradi, token aynan shu ID uchun chiqariladi va Android client ID'si azp claim'ida qoladi, audda emas;

  • iOS — aud standart holatda iOS client ID bo'ladi. Ilova GIDServerClientIDni Web client ID qilib sozlagan bo'lsa, aud ham Web client ID'ga aylanadi.

Shuning uchun backend bitta ID'ni emas, allowlistni biladi: odatda Web va iOS client ID'lari. Ro'yxat env'dan keladi, kodda qattiq yozilmaydi — dev, staging va production'da u har xil:

config/settings.py
import os

from django.core.exceptions import ImproperlyConfigured

INSTALLED_APPS = [
    # ...
    "apps.accounts",
]

# vergul bilan: "123-web.apps.googleusercontent.com,123-ios.apps.googleusercontent.com"
GOOGLE_CLIENT_IDS = [
    cid.strip() for cid in os.environ.get("GOOGLE_CLIENT_IDS", "").split(",") if cid.strip()
]
if not GOOGLE_CLIENT_IDS:
    # bo'sh ro'yxat — hamma login yiqiladi. Buni deploy paytida bilgan
    # yaxshi, birinchi foydalanuvchi shikoyat qilganda emas.
    # CI va test muhitiga ham qiymat berib qo'ying (.env.example)
    raise ImproperlyConfigured("GOOGLE_CLIENT_IDS bo'sh")

Bitta ID o'rniga ro'yxat yozish ortiqcha ish emas: u keyingi bo'limdagi eng ko'p uchraydigan production xatosining oldini oladi. Ro'yxat env'dan kelayotgani ham muhim — client ID dev, staging va production'da har xil, va uni almashtirish uchun yangi reliz kutib o'tirmaysiz.

«Android'da ishlayapti, iOS'da 401» sindromi

Eng ko'p uchraydigan production bug'i: sozlamada faqat Web client ID turadi (Android shu bilan ishlaydi), iOS ilova esa GIDServerClientIDsiz yig'ilgan — uning token'ida aud iOS client ID, verify yiqiladi. Allowlist qilib qo'ying va ikkala platformadan kelgan token'ni sinab ko'ring. audni bilishning eng tez yo'li: rad etilgan token'ni jwt.ioda emas, log'dagi xato matnida ko'rish — kutubxona "Token has wrong audience X, expected one of Y" deb aynan ikkala qiymatni yozadi.

2-qadam: verify funksiyasi#

Butun xavfsizlik shu faylda. Modul alohida turadi: view uni chaqiradi, testlar uni mock qiladi, ertaga Apple Sign-In qo'shilsa — yonida apple.py paydo bo'ladi.

Kod to'rtga bo'linadi: google.py token'ni verify qiladi, models.py sub bilan User orasidagi bog'lanishni saqlaydi, services.py identity'dan foydalanuvchi yasaydi, views.py esa HTTP qatlami bo'lib qoladi. Bu bo'linish testlar uchun ham qulay: ertaga Apple Sign-In qo'shilsa, yonida apple.py paydo bo'ladi, qolgan uchtasiga tegilmaydi.

Kerakli kutubxonalar uchta. cachecontrol ro'yxatda bejiz turmagan — nega kerakligini kod ostida alohida tushuntiraman:

Terminal
uv add google-auth cachecontrol djangorestframework-simplejwt

Kodda ikki narsaga diqqat qiling: audience argumenti va modul darajasidagi session. Ikkalasi ham "ishlayapti-ku" deb e'tibordan chetda qoladigan, lekin production'da qimmatga tushadigan joylar:

apps/accounts/google.py
from dataclasses import dataclass

import cachecontrol
import requests
from django.conf import settings
from google.auth import exceptions as google_exceptions
from google.auth.transport import requests as google_requests
from google.oauth2 import id_token as google_id_token


class GoogleAuthError(Exception):
    """id_token yaroqsiz — sababi client'ga ochilmaydi."""


class GoogleUnavailable(Exception):
    """Google'ning certs endpoint'iga yetib bo'lmadi — token aybdor emas."""


@dataclass(frozen=True)
class GoogleIdentity:
    sub: str  # Google hisobining O'ZGARMAS ID'si
    email: str
    name: str
    picture: str


# Bitta session butun process uchun. cachecontrol Google javobidagi
# Cache-Control'ni o'qib, public key'larni xotirada saqlaydi
_cached_session = cachecontrol.CacheControl(requests.Session())
_google_request = google_requests.Request(session=_cached_session)


def verify_google_token(raw_token: str) -> GoogleIdentity:
    try:
        # verify_oauth2_token uch ishni birdan qiladi:
        # 1) Google'ning public key'lari bilan SIGNATURE tekshiruvi
        # 2) exp va iat tekshiruvi
        # 3) iss accounts.google.com (yoki https://accounts.google.com) ekani
        claims = google_id_token.verify_oauth2_token(
            raw_token,
            _google_request,
            # audience list ham bo'ladi — kutubxonaning o'zi allowlist bilan
            # solishtiradi. Bu yerga None yozilsa, `aud` UMUMAN tekshirilmaydi
            audience=settings.GOOGLE_CLIENT_IDS,
            # server soati Google'nikidan bir necha soniya farq qilsa,
            # yangi token "Token used too early" bilan rad etilmasin
            clock_skew_in_seconds=10,
        )
    except google_exceptions.TransportError as exc:
        # Google javob bermadi. Bu "token yaroqsiz" degani EMAS —
        # shuning uchun alohida xato (pastda 503 bo'lib qaytadi)
        raise GoogleUnavailable(str(exc)) from exc
    except (ValueError, google_exceptions.GoogleAuthError) as exc:
        # signature, exp/iat va aud xatolari ValueError bo'lib keladi
        # (InvalidValue, MalformedError — ikkalasi ham ValueError avlodi).
        # Wrong issuer esa sof GoogleAuthError: uni ushlamasak,
        # endpoint 401 o'rniga 500 qaytaradi
        raise GoogleAuthError(str(exc)) from exc

    if not claims.get("email_verified", False):
        raise GoogleAuthError("email_verified = false")

    email = claims.get("email")
    if not email:
        # email scope so'ralmagan bo'lsa, claim umuman kelmaydi:
        # claims["email"] bu yerda KeyError va 500 bo'lardi
        raise GoogleAuthError("email claim yo'q")

    return GoogleIdentity(
        sub=claims["sub"],
        email=email,
        name=claims.get("name", ""),
        picture=claims.get("picture", ""),
    )

Funksiya qaytargan GoogleIdentity — endi ishonch bilan foydalanish mumkin bo'lgan ma'lumot: u signature'dan, expdan, issdan va auddan o'tgan. Shu to'rttadan bittasi tushib qolsa, qolgan uchtasi uning o'rnini qoplamaydi. Eng nozik joyi esa — bitta argument.

audience=None — mana shu bitta so'z butun tekshiruvni o'chiradi

google-auth hujjatida aniq yozilgan: "If None then the audience is not verified". Ya'ni signature to'g'ri, iss to'g'ri, exp to'g'ri — lekin token butunlay boshqa ilova uchun berilgan bo'lishi mumkin. Hujumchi o'zining Google client ID'si bilan qurilgan istalgan ilovadan token olib, sizning backend'ingizga kiradi. Xavfsizlik uchun yozilgan kod xavfsizlikni o'chirib qo'ygan bo'ladi. Qoida: audience argumentiga doim allowlist beriladi, settings.GOOGLE_CLIENT_IDS or None kabi "himoya" esa aynan teskari ishlaydi — ro'yxat bo'shab qolgan kuni tekshiruv o'chadi.

Endi session haqida. google-auth hujjatining birinchi abzasi shuni aytadi: "By default, this will re-fetch certificates for each verification". Ya'ni har bir login https://www.googleapis.com/oauth2/v1/certs ga bitta tashqi HTTPS so'rov degani. Foydalanuvchi uchun bu qo'shimcha kutish, siz uchun — har login'ni Google'ning javob berishiga bog'lab qo'yish.

Cache — tezlik uchun emas, chidamlilik uchun

Google o'sha javobni Cache-Control: public, max-age=20191 bilan beradi — ya'ni o'zi "bu kalitlarni olti soatcha saqlasang bo'ladi" deydi. Cache qilish xavfsizlikni pasaytirmaydi. Cache'siz qolsangiz, Google tomonda qisqa uzilish yuz berganda hamma login bir vaqtda yiqiladi va sizning access log'ingiz aybdorni ko'rsatmaydi. Har gunicorn worker'ining o'z process'i, demak o'z cache'i bo'ladi — bu normal, kalit kichkina. Umumiy Redis cache kerak bo'ladigan hajm emas.

3-qadam: identity — sub bo'yicha, email bo'yicha emas#

Foydalanuvchini bazada sub claim bo'yicha taniymiz. sub — Google hisobining o'zgarmas ID'si; email esa o'zgaradi (Google Workspace'da ayniqsa). Email'ga bog'langan tizim, odam pochtasini o'zgartirgan kuni unga yangi hisob ochib beradi — eski buyurtmalari, sevimli stansiyalari "yo'qoladi".

apps/accounts/models.py
from django.conf import settings
from django.db import models


class GoogleAccount(models.Model):
    """Google hisobi ↔ bizning User orasidagi bog'lanish."""

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="google_account",
    )
    # unique — nafaqat toza ma'lumot uchun: pastda race condition'ni
    # aynan shu constraint to'xtatadi
    sub = models.CharField(max_length=255, unique=True)
    email = models.EmailField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.email} ({self.sub})"

Model bitta vazifani bajaradi: subni Userga bog'laydi va uni butun baza bo'ylab yagona qilib qo'yadi. email bu yerda ham saqlanadi, lekin nusxa sifatida — u qidiruv kaliti emas, oxirgi ko'rilgan qiymat. Nega buni oddiygina usernamega yozib qo'ymaslik kerakligini alohida aytib o'taman.

Nega sub to'g'ridan-to'g'ri username maydoniga tiqilmaydi

Eng qisqa yo'l — subni username maydoniga tiqish. U ishlaydi, lekin identity boshqa modulning maydonida yashiringan bo'ladi: ertaga Apple Sign-In qo'shilsa, username formatini parse qilib qidirishga tushasiz; admin panelda odam kimligi ko'rinmaydi; subni unique qilishni esa Django'ning username unique'iga ishonib topshirasiz. To'g'ri javob — USERNAME_FIELD = "email" bo'lgan custom User, lekin uni loyihaning birinchi kuni qilish kerak edi: birinchi migratedan keyin custom User'ga o'tish og'riqli ko'chirish. 4-qismda kech — shuning uchun identity alohida modelda turadi, username esa shunchaki to'ldiriladigan texnik maydon bo'lib qoladi.

Endi identitydan User yasaymiz. Ikki nozik joy bor: email har kirishda yangilanadi (odam Google'da o'zgartirgan bo'lsa) va ikki qurilmadan bir vaqtda kelgan birinchi login ikkita hisob yaratib qo'ymasligi kerak:

apps/accounts/services.py
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction

from .google import GoogleIdentity
from .models import GoogleAccount

User = get_user_model()


def get_or_create_user(identity: GoogleIdentity) -> User:
    account = GoogleAccount.objects.select_related("user").filter(sub=identity.sub).first()

    if account is None:
        try:
            with transaction.atomic():
                user = User.objects.create_user(
                    username=f"google-{identity.sub}",
                    email=identity.email,
                    first_name=identity.name[:150],
                )
                account = GoogleAccount.objects.create(
                    user=user, sub=identity.sub, email=identity.email
                )
        except IntegrityError:
            # ikki qurilma bir vaqtda birinchi marta kirdi: ikkovi ham
            # "yo'q ekan" deb topdi, ikkinchi INSERT unique(sub) ga urildi.
            # transaction.atomic bo'lmasa — yarim yaratilgan User qolib ketadi
            account = GoogleAccount.objects.select_related("user").get(sub=identity.sub)

    elif account.email != identity.email:
        account.email = identity.email
        account.save(update_fields=["email"])
        User.objects.filter(pk=account.user_id).update(email=identity.email)

    return account.user

Funksiyada birorta joyda email bo'yicha qidiruv yo'qligiga e'tibor bering — bu ataylab shunday. GoogleAccount.objects.filter(email=...) bu yerga bitta qator bo'lib qo'shiladi va butun tizimni boshqa xavfsizlik sinfiga tushirib yuboradi.

Email bo'yicha avtomatik bog'lash — account takeover'ning klassik yo'li

"Bu email allaqachon bazada bor ekan, o'sha hisobga ulab yuboraman" degan bir qator kod — eng ko'p uchraydigan zaifliklardan biri. Provayder email'ni tasdiqlamagan bo'lsa, hujumchi qurbonning email'i bilan hisob ochib, uning kabinetiga kiradi. Biz email_verifiedni tekshiramiz, lekin qoida baribir o'z kuchida: bog'lash — foydalanuvchining ongli amali. Mavjud hisobga Google'ni ulash kerak bo'lsa, buni kirgan foydalanuvchi o'z profilidan qiladi, login endpoint'i emas.

4-qadam: endpoint#

View'ning o'zi qisqa: verify qil, xatolarni to'g'ri status'ga ayir, token qaytar. Diqqat qiladigan joy — throttle_classes. Bu ochiq endpoint: har so'rov crypto hisob-kitob, cache sovuq bo'lsa tashqi so'rov ham degani. Throttle'siz qoldirilgan login endpoint'i — arzon DoS nishoni:

apps/accounts/serializers.py
from rest_framework import serializers


class GoogleAuthSerializer(serializers.Serializer):
    id_token = serializers.CharField()

Serializer bitta ish qiladi: id_token maydoni umuman kelganini tekshiradi. U bo'lmasa, DRF 400 qaytaradi va biz bo'sh satrni verify qilishga urinmaymiz. 400 bilan 401 bu yerda har xil hodisa: birinchisi — mobil ilovaning bug'i, ikkinchisi — token'ning o'zi yaroqsizligi.

apps/accounts/views.py
import logging

from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken

from .google import GoogleAuthError, GoogleUnavailable, verify_google_token
from .serializers import GoogleAuthSerializer
from .services import get_or_create_user

logger = logging.getLogger(__name__)


class GoogleAuthThrottle(AnonRateThrottle):
    scope = "google_auth"


class GoogleAuthView(APIView):
    permission_classes = [AllowAny]
    throttle_classes = [GoogleAuthThrottle]

    def post(self, request):
        serializer = GoogleAuthSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        try:
            identity = verify_google_token(serializer.validated_data["id_token"])
        except GoogleUnavailable as exc:
            # 401 bersak, mobil ilova foydalanuvchini tizimdan chiqarib
            # yuboradi — aybi yo'q odamni. 503 esa "keyinroq urinib ko'ring"
            logger.error("Google certs endpoint javob bermadi: %s", exc)
            return Response(
                {"detail": "Google bilan bog'lanib bo'lmadi"},
                status=status.HTTP_503_SERVICE_UNAVAILABLE,
            )
        except GoogleAuthError as exc:
            # sababni client'ga ochmaymiz — hujumchi uchun qimmatli ma'lumot.
            # Log'ga ham raw token EMAS, faqat sabab yoziladi:
            # log fayli o'g'irlansa, token bilan birga hisob ham ketadi
            logger.warning("id_token rad etildi: %s", exc)
            return Response(
                {"detail": "id_token yaroqsiz"},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        user = get_or_create_user(identity)
        refresh = RefreshToken.for_user(user)
        return Response({"access": str(refresh.access_token), "refresh": str(refresh)})

Qolgani — URL va migratsiya. Endpoint manzili config/urls.py dagi ro'yxatga ulanadi: path("api/", include("apps.accounts.urls")).

apps/accounts/urls.py
from django.urls import path

from .views import GoogleAuthView

urlpatterns = [
    path("auth/google/", GoogleAuthView.as_view()),
]

accounts endi model'li app — migratsiyani unutmang. Aks holda birinchi login relation "accounts_googleaccount" does not exist bilan yiqiladi va xato log'da Google bilan bog'liqdek ko'rinadi, aslida sabab bazada:

Terminal
uv run python manage.py makemigrations accounts
uv run python manage.py migrate

Mobil tomondan bu bitta so'rov. Lokal muhitda http:// bilan sinaysiz, production'da esa faqat HTTPS: id_token ham, javobdagi access/refresh ham ochiq matnda ketadi, ularni ushlagan odam hisobni qo'lga oladi:

Terminal
curl -X POST http://127.0.0.1:8000/api/auth/google/ \
  -H "Content-Type: application/json" \
  -d '{"id_token": "eyJhbGciOiJSUzI1NiIs..."}'

# javob:
# {"access": "eyJ...", "refresh": "eyJ..."}

Javobdan keyin ilova barcha so'rovlarni Authorization: Bearer <access> header'i bilan yuboradi — 3-qismdagi mehmon-qurilma ham endi userga bog'lanadi (login paytida ilova token ro'yxatga olish endpoint'ini qayta chaqiradi). Buning uchun DRF'ga Bearer header'ini o'qishni aytish kerak — simplejwt'ni o'rnatishning o'zi yetmaydi:

config/settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    # GoogleAuthThrottle.scope shu yerdan qiymat oladi
    "DEFAULT_THROTTLE_RATES": {"google_auth": "10/min"},
}

Bu sozlamasiz /api/devices/ Bearer token'ni jimgina e'tiborsiz qoldiradi: javob baribir 201, lekin request.user anonim va qurilma hech qachon foydalanuvchiga bog'lanmaydi. Sozlama bilan mehmon (header'siz) so'rovlar avvalgidek o'tadi, yaroqsiz Bearer esa 401 oladi.

5-qadam: testlar — Google'siz#

Tarmoqqa chiqmasdan verify_oauth2_tokenni mock qilamiz. Lekin mock bilan bitta xavf bor: kutubxonani almashtirib qo'ysak, unga qanday argument berganimiz tekshirilmay qoladi — ya'ni audience yo'qolib qolsa ham testlar yashil bo'laveradi. Shuning uchun birinchi testda call_argsni ham tekshiramiz:

apps/accounts/tests.py
from unittest.mock import patch

import pytest
from django.contrib.auth import get_user_model
from django.core.cache import cache
from google.auth import exceptions as google_exceptions

from apps.accounts.models import GoogleAccount

VERIFY = "apps.accounts.google.google_id_token.verify_oauth2_token"
CLAIMS = {
    "sub": "1122334455",
    "aud": "test-web.apps.googleusercontent.com",
    "email": "user@misol.uz",
    "email_verified": True,
    "name": "Test User",
}


@pytest.fixture
def client_ids(settings):
    settings.GOOGLE_CLIENT_IDS = ["test-web.apps.googleusercontent.com"]
    # throttle hisoblagichi cache'da turadi: tozalamasak, testlar
    # bir-birini 429 qilib yiqitadi
    cache.clear()


def login(client):
    return client.post("/api/auth/google/", {"id_token": "raw"}, content_type="application/json")


@patch(VERIFY, return_value=CLAIMS)
def test_valid_token_creates_user_and_returns_jwt(mock_verify, client, client_ids, db):
    response = login(client)

    assert response.status_code == 200
    assert {"access", "refresh"} <= response.json().keys()
    # aud tekshiruvi kutubxonaga TOPSHIRILGANINI qayd qilamiz
    assert mock_verify.call_args.kwargs["audience"] == ["test-web.apps.googleusercontent.com"]
    assert GoogleAccount.objects.get(sub="1122334455").user.email == "user@misol.uz"


@pytest.mark.parametrize(
    "error",
    [
        google_exceptions.InvalidValue("Token has wrong audience"),
        google_exceptions.GoogleAuthError("Wrong issuer."),
        ValueError("Could not verify token signature."),
    ],
)
def test_invalid_token_is_401_and_creates_nothing(error, client, client_ids, db):
    with patch(VERIFY, side_effect=error):
        response = login(client)

    assert response.status_code == 401
    assert get_user_model().objects.count() == 0


@patch(VERIFY, side_effect=google_exceptions.TransportError("certs unreachable"))
def test_google_down_is_503_not_401(mock_verify, client, client_ids, db):
    # foydalanuvchi aybdor emas — uni tizimdan chiqarib yubormaymiz
    assert login(client).status_code == 503


@patch(VERIFY, return_value={**CLAIMS, "email_verified": False})
def test_unverified_email_rejected(mock_verify, client, client_ids, db):
    assert login(client).status_code == 401


@patch(VERIFY, return_value=CLAIMS)
def test_second_login_reuses_the_same_user(mock_verify, client, client_ids, db):
    login(client)
    login(client)

    assert get_user_model().objects.count() == 1
    assert GoogleAccount.objects.count() == 1

Bu besh test butun zanjirni qoplaydi: to'g'ri token, uch xil yaroqsiz token, Google'ning uzilishi, tasdiqlanmagan email va takroriy login. Qo'lda tekshiriladigan yagona narsa qoladi — haqiqiy qurilmadan kelgan haqiqiy token, ikkala platformada ham:

Terminal
uv run pytest apps/accounts/ -q

Xulosa#

  • id_tokenga faqat verify'dan keyin ishoniladi — decode ≠ verify;
  • audience argumentiga allowlist beriladi; None — aud tekshiruvini butunlay o'chiradi;

  • public key'lar cache'lanadi: cachecontrol session'siz har login Google'ga tashqi so'rov va Google uzilsa hamma login yiqiladi;

  • identity kaliti — sub; email o'zgaruvchan ko'rsatkich, unga bog'lash account takeover'ga olib boradi;

  • Google javob bermasa 503, token yaroqsiz bo'lsa 401 — ikkovi ikki xil hodisa;

  • ochiq login endpoint'i throttle bilan yopiladi, xato sabablari esa log'da qoladi, javobda emas.

Seriyaning yakuniy qismida umumlashtiramiz: backend'ni mobil ilovaga moslab dizayn qilish — versiyalash, payload hajmi, sekin tarmoqlar va offline'ga chidamlilik.

24 daqiqa 240 marta o‘qildi