Maqola · 2026-08-16 · ~11 daqiqa o‘qiladi · murakkab
Google Sign-In: iOS va Android tokenini backend'da to'g'ri tekshirish
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 haqiqiyligini tekshirmasak — istalgan odam istalgan
email nomidan kiradi.
Oqim shunday:
Mobil ilova ──(Google SDK)──► Google ──► id_token (JWT)
│
└──POST /api/auth/google/ {"id_token": "..."}──► bizning backend
│
1) token imzosini Google kaliti bilan tekshirish
2) token AYNAN BIZNING ilovaga berilganini tekshirish
3) foydalanuvchini topish/yaratish
4) o'zimizning access/refresh JWT'ni qaytarish
Eng xavfli xato: tokenni shunchaki decode qilish
id_token — oddiy JWT, uni istalgan kutubxona imzosiz ham o'qib beradi
(jwt.decode(..., options={"verify_signature": False})). Ichidan chiroyli
email chiqadi — va tekshirmasdan ishongan backend'ga istalgan qalbaki token
o'tadi. Token faqat Google'ning ochiq kaliti bilan imzo tekshiruvidan
o'tgandagina haqiqiy. JWT tuzilishini eslash uchun:
JWT autentifikatsiya.
Nega o'z JWT'imizni beramiz#
Google tokeni — shaxsni isbotlash hujjati, API'ga kirish chiptasi emas: muddati ~1 soat, uni yangilash Google SDK'ga bog'liq, ichida bizning rollarimiz/ID'larimiz yo'q. Shuning uchun sxema ikki bosqichli: Google tokenini bir marta tekshiramiz, keyin foydalanuvchiga o'zimizning access/refresh juftligimizni beramiz — API hayoti oddiy JWT autentifikatsiya bilan davom etadi.
1-qadam: sozlamalar — ikki platforma, ikki client ID#
Google Cloud Console'da mobil jamoa ikkita OAuth client yaratadi: biri Android, biri iOS uchun. Ikkalasining client ID'si har xil — va ikkalasidan kelgan token ham bizga yaroqli bo'lishi kerak:
uv add google-auth djangorestframework-simplejwt
# config/settings.py
import os
# vergul bilan: "123-android.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()
]
«Android'da ishlayapti, iOS'da 401» sindromi
Eng ko'p uchraydigan production bug'i: sozlamada faqat Android client ID
turadi, iOS'dan kelgan tokenning aud maydoni esa boshqa — tekshiruv
yiqiladi. Ro'yxat qilib qo'ying va ikkala platformani testda qamrang.
2-qadam: tekshiruv funksiyasi#
apps/accounts/
├── __init__.py
├── models.py ← standart User (yoki custom) — bu maqolada o'zgarmaydi
├── google.py ← id_token tekshiruvi (hozir shu yerdamiz)
├── serializers.py
├── views.py
└── tests.py
# apps/accounts/google.py
from dataclasses import dataclass
from django.conf import settings
from google.auth.transport import requests as google_requests
from google.oauth2 import id_token as google_id_token
class GoogleAuthError(Exception):
"""Token yaroqsiz — sababi foydalanuvchiga batafsil aytilmaydi."""
@dataclass(frozen=True)
class GoogleIdentity:
sub: str # Google'ning O'ZGARMAS foydalanuvchi ID'si
email: str
name: str
picture: str
def verify_google_token(raw_token: str) -> GoogleIdentity:
try:
# verify_oauth2_token uch ishni birdan qiladi:
# 1) Google JWKS kalitlari bilan IMZO tekshiruvi (kalitlarni keshlaydi)
# 2) muddat (exp) tekshiruvi
# 3) issuer accounts.google.com ekani
claims = google_id_token.verify_oauth2_token(
raw_token,
google_requests.Request(),
# audience=None — aud'ni o'zimiz tekshiramiz (ro'yxat bilan)
audience=None,
)
except ValueError as exc:
raise GoogleAuthError("token imzosi yoki muddati yaroqsiz") from exc
# aud — token qaysi ilovaga berilgan. Bizning ikki client ID'dan biri
# bo'lmasa, bu token BOSHQA ilova uchun olingan — qabul qilmaymiz
if claims.get("aud") not in settings.GOOGLE_CLIENT_IDS:
raise GoogleAuthError("token boshqa ilovaga tegishli")
if not claims.get("email_verified", False):
raise GoogleAuthError("email tasdiqlanmagan")
return GoogleIdentity(
sub=claims["sub"],
email=claims["email"],
name=claims.get("name", ""),
picture=claims.get("picture", ""),
)
sub — ha, email — yo'q
Foydalanuvchini bazada sub bo'yicha taniymiz, email bo'yicha emas.
Email o'zgarishi mumkin (Google Workspace'da ayniqsa) — o'zgargan kuni
email'ga bog'langan tizim o'sha odamga yangi hisob ochib beradi, eski
ma'lumotlari "yo'qoladi". sub esa hisob umri davomida o'zgarmas.
3-qadam: foydalanuvchi va endpoint#
# apps/accounts/serializers.py
from rest_framework import serializers
class GoogleAuthSerializer(serializers.Serializer):
id_token = serializers.CharField()
# apps/accounts/views.py
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from .google import GoogleAuthError, verify_google_token
from .serializers import GoogleAuthSerializer
User = get_user_model()
class GoogleAuthView(APIView):
permission_classes = [AllowAny]
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 GoogleAuthError:
# sababni ochmaymiz — hujumchi uchun qimmatli ma'lumot bo'lardi
return Response(
{"detail": "Google token yaroqsiz"},
status=status.HTTP_401_UNAUTHORIZED,
)
# username sifatida sub — o'zgarmas kalit (yuqoridagi eslatma).
# email/ism har kirishda yangilanadi: foydalanuvchi Google'da
# o'zgartirgan bo'lsa, bizda ham yangi ko'rinadi
user, _ = User.objects.update_or_create(
username=f"google:{identity.sub}",
defaults={"email": identity.email, "first_name": identity.name[:150]},
)
refresh = RefreshToken.for_user(user)
return Response({"access": str(refresh.access_token), "refresh": str(refresh)})
# apps/accounts/urls.py
from django.urls import path
from .views import GoogleAuthView
urlpatterns = [
path("auth/google/", GoogleAuthView.as_view()),
]
Mobil tomondan bu bitta so'rov:
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..."}
Shu javobdan keyin ilova barcha so'rovlarni Authorization: Bearer <access>
bilan yuboradi — 3-qismdagi mehmon-qurilma
ham endi userga bog'lanadi (login paytida ilova token ro'yxatga olish
endpoint'ini qayta chaqiradi).
4-qadam: test — Google'siz#
Tarmoqqa chiqmasdan verify_oauth2_tokenni mock qilamiz:
# apps/accounts/tests.py
from unittest.mock import patch
import pytest
from django.contrib.auth import get_user_model
CLAIMS = {
"sub": "1122334455",
"aud": "test-android.apps.googleusercontent.com",
"email": "user@misol.uz",
"email_verified": True,
"name": "Test User",
}
@pytest.fixture
def client_ids(settings):
settings.GOOGLE_CLIENT_IDS = ["test-android.apps.googleusercontent.com"]
@patch("apps.accounts.google.google_id_token.verify_oauth2_token", return_value=CLAIMS)
def test_valid_token_creates_user_and_returns_jwt(mock_verify, client, client_ids, db):
response = client.post(
"/api/auth/google/", {"id_token": "raw"}, content_type="application/json"
)
assert response.status_code == 200
assert {"access", "refresh"} <= response.json().keys()
user = get_user_model().objects.get(username="google:1122334455")
assert user.email == "user@misol.uz"
@patch(
"apps.accounts.google.google_id_token.verify_oauth2_token",
return_value={**CLAIMS, "aud": "hujumchi-ilova.apps.googleusercontent.com"},
)
def test_foreign_audience_rejected(mock_verify, client, client_ids, db):
response = client.post(
"/api/auth/google/", {"id_token": "raw"}, content_type="application/json"
)
assert response.status_code == 401
assert get_user_model().objects.count() == 0
Takroriy kirish — takroriy foydalanuvchi emas
update_or_create(username=f"google:{sub}") ikkinchi kirishda yangi qator
yaratmaydi — mavjudini yangilaydi. Buni ham testlab qo'ygan ma'qul: bitta
sub bilan ikki marta kirib, User.objects.count() == 1 ekanini tekshiring.
Xulosa#
id_tokenga faqat imzo tekshiruvidan keyin ishoniladi — decode ≠ verify;audro'yxat bilan tekshiriladi: iOS va Android client ID'lari har xil;- Foydalanuvchi kaliti —
sub; email — o'zgaruvchan ko'rsatkich; - Google tokeni bir martalik isbot, API'da esa o'z access/refresh JWT'ingiz ishlaydi;
- Xato sabablarini mijozga batafsil qaytarmang — 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.