Google OAuth / Social Login

~25 daq · NestJS · Autentifikatsiya

Muammodan boshlaylik

Foydalanuvchi "Google bilan kirish" tugmasini bosdi. Qanday ishlaydi? Siz Google parolini so'ramaysiz — Google o'zi tekshiradi. Muvaffaqiyatli bo'lsa, Google sizga foydalanuvchi ma'lumotlarini yuboradi. Siz esa token yaratasiz.

Bu — OAuth 2.0. Uchinchi tomon (Google) autentifikatsiyani o'z zimmasiga oladi, siz faqat natijadan foydalanasiz.


OAuth 2.0 oqimi

1. Foydalanuvchi "Google bilan kiring" bosadi
         │
         ▼
2. Backend Google ga yo'naltiradi
   https://accounts.google.com/o/oauth2/auth?client_id=...
         │
         ▼
3. Foydalanuvchi Google da login qiladi, ruxsat beradi
         │
         ▼
4. Google sizning callback URL ga code yuboradi
   GET /auth/google/callback?code=4/P7q7W91...
         │
         ▼
5. Backend code → Google token oladi
         │
         ▼
6. Google token → foydalanuvchi profili (email, name, photo)
         │
         ▼
7. Bazada foydalanuvchini topasiz yoki yaratasiz
         │
         ▼
8. O'z JWT tokeningizni yaratasiz, qaytarasiz

Hayotiy o'xshatma: yangi ofisga kirishda qo'riqchi (Google) pasportingizni tekshiradi. "Bu odam haqiqiy" degan tasdiqlama beriladi. Ofis esa o'z kirish kartasini (JWT) chiqaradi. Passport bilan emas, kartangiz bilan yurasiz.


Google Console da sozlash

  1. console.cloud.google.com ga kiring
  2. Yangi loyiha yarating yoki mavjudini tanlang
  3. APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID
  4. Application type: Web application
  5. Authorized redirect URIs: http://localhost:3000/auth/google/callback
  6. Client ID va Client Secret ni .env ga saqlang
# .env
GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-abcdef123456
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

O'rnatish

npm install passport-google-oauth20
npm install -D @types/passport-google-oauth20

Google Strategiya

// src/auth/strategies/google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../auth.service';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(
    private config: ConfigService,
    private authService: AuthService,
  ) {
    super({
      clientID:     config.get('GOOGLE_CLIENT_ID'),
      clientSecret: config.get('GOOGLE_CLIENT_SECRET'),
      callbackURL:  config.get('GOOGLE_CALLBACK_URL'),
      scope: ['email', 'profile'], // ← Google dan nima so'raymiz
    });
  }

  async validate(
    accessToken: string,  // Google access token — bizga kerak emas
    refreshToken: string, // Google refresh token — bizga kerak emas
    profile: any,         // foydalanuvchi profili
    done: VerifyCallback,
  ) {
    const { emails, name, photos } = profile;

    const googleUser = {
      email:     emails[0].value,
      firstName: name.givenName,
      lastName:  name.familyName,
      photo:     photos[0].value,
      googleId:  profile.id,
    };

    // Bazadan topamiz yoki yaratamiz
    const user = await this.authService.findOrCreateGoogleUser(googleUser);
    done(null, user); // ← req.user ga qo'yiladi
  }
}

scope: ['email', 'profile'] — Google dan faqat email va profil ma'lumotlarini so'raymiz. Boshqa ruxsatlar (calendar, drive) kerak bo'lsa shu yerga qo'shiladi.


AuthService — Google foydalanuvchini topish yoki yaratish

// src/auth/auth.service.ts
async findOrCreateGoogleUser(googleUser: {
  email: string;
  firstName: string;
  lastName: string;
  photo: string;
  googleId: string;
}) {
  // Avval email bo'yicha topamiz
  let user = await this.usersService.findByEmail(googleUser.email);

  if (!user) {
    // Yo'q bo'lsa — yangisini yaratamiz
    user = await this.usersService.createGoogleUser({
      name: `${googleUser.firstName} ${googleUser.lastName}`,
      email: googleUser.email,
      photo: googleUser.photo,
      googleId: googleUser.googleId,
      // parol yo'q — Google foydalanuvchi parol bilan kira olmaydi
    });
  }

  return user;
}

User sxemasiga Google maydonlari

// user.schema.ts
@Schema({ timestamps: true })
export class User {
  @Prop({ required: true })
  name: string;

  @Prop({ required: true, unique: true })
  email: string;

  @Prop({ select: false, default: null })
  password: string | null; // Google foydalanuvchilarda null

  @Prop({ default: null })
  googleId: string | null;

  @Prop({ default: null })
  photo: string | null;

  @Prop({ default: 'local', enum: ['local', 'google'] })
  provider: string; // qaysi usulda ro'yxatdan o'tgan
}

Guard va Controller

// src/auth/guards/google-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {}
// src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  // GET /auth/google — Google ga yo'naltirish
  @Get('google')
  @Public()
  @UseGuards(GoogleAuthGuard)
  googleLogin() {
    // Bu metod hech narsa qilmaydi
    // GoogleAuthGuard foydalanuvchini Google ga yo'naltiradi
  }

  // GET /auth/google/callback — Google qayta yo'naltiradi
  @Get('google/callback')
  @Public()
  @UseGuards(GoogleAuthGuard)
  async googleCallback(@Req() req, @Res() res) {
    // req.user — GoogleStrategy.validate() dan kelgan
    const tokens = await this.authService.login(req.user);

    // Frontend ga token bilan yo'naltirish
    const frontendUrl = process.env.FRONTEND_URL;
    res.redirect(`${frontendUrl}/auth/callback?token=${tokens.accessToken}`);
  }
}

Nima uchun res.redirect? OAuth callback browser orqali keladi. JSON javob qaytarib bo'lmaydi — brauzer uni ko'rsatmaydi. Frontend ga redirect qilib, tokenni URL yoki cookie orqali uzatamiz.


Auth Module ga qo'shish

// src/auth/auth.module.ts
@Module({
  imports: [ PassportModule, UsersModule, JwtModule.registerAsync({...}) ],
  providers: [
    AuthService,
    LocalStrategy,
    JwtStrategy,
    JwtRefreshStrategy,
    GoogleStrategy, // ← qo'shildi
  ],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

Tez-tez uchraydigan xatolar

Callback URL mos kelmaydi

# .env da
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

# Google Console da ham aynan shu URL bo'lishi kerak
# ← bittasi ham boshqacha bo'lsa — "redirect_uri_mismatch" xatosi

Bir xil email, ikki xil provider

// Foydalanuvchi avval parol bilan ro'yxatdan o'tgan,
// keyin Google bilan kirmoqchi bo'ldi — bir xil email

// ❌ Yangi user yaratish — ikkita hisob bir emailda
// ✅ Topib, Google ID ni ulab yangilash
const user = await this.usersService.findByEmail(googleUser.email);
if (user && !user.googleId) {
  await this.usersService.linkGoogle(user._id, googleUser.googleId);
}

Callback da token URL da ochiq qoladi

❌ /callback?token=eyJ...  — URL loglarida, brauzer tarixida qoladi

✅ httpOnly cookie — xavfsizroq:
   res.cookie('accessToken', tokens.accessToken, {
     httpOnly: true,
     secure: process.env.NODE_ENV === 'production',
     sameSite: 'lax',
     maxAge: 15 * 60 * 1000, // 15 daqiqa
   });
   res.redirect(`${frontendUrl}/dashboard`);

Xulosa

Google OAuth — foydalanuvchi parolini siz bilmaysiz, Google tekshiradi. Muvaffaqiyatli bo'lsa Google profil ma'lumotlarini yuboradi, siz o'z JWT tokeningizni yaratasiz.

Eslab qoling:

  • Google Console da callback URL to'g'ri yozilishi shart
  • findOrCreateGoogleUser — topadi yoki yaratadi, ikki marta yaratmaydi
  • provider maydoni — 'local' yoki 'google'
  • Bir email, ikki provider — birlashtiring, ikkita hisob yaratmang
  • Callback da res.redirect — JSON qaytarib bo'lmaydi
  • Token URL da emas, httpOnly cookie da saqlash xavfsizroq

Keyingi dars

29-dars: Serialization — response filtering, parolni yashirish