Nodejs: Nest ile Google Oauth Implementasyonu

Giriş

Hepinize Merhabalar👋
Bu yazımızda discord, google, facebook ve diğer sosyal platformdaki hesapların şifrelerini istemeden, bize sunulan oauth servisleri ile kullanıcı bilgilerini çekeceğiz.

Ben bu yazıda sadece google'i anlatmaya çalışacağım.
Çok fazla detaya inmeyeceğim, dilerseniz kaynakça kısmından derine inebilirsiniz.

Direkt olarak projeyi github üzerinden incelemek için buraya, test etmek içinde buraya
tıklayabilirsiniz.


Kullanacağımız Kütüphane Ve Servisler

  1. nestjs
  2. passport-google-oauth20

Database seçimi:

  1. mongodb
    Not: Eğer mongodb uzak sunucu bağlantısını nasıl alacağınızı bilmiyorsanız, buraya tıklayarak ilgili yazıya ulaşabilirsiniz.

Google Oauth Servislerinin Aktif Edilmesi

  1. Google Console Cloud'a giriş yapıyoruz.(Buraya tıklayarak hızlıca gidebilirsiniz.)
  2. Proje seçimini yapıyoruz.
  3. Sol menüden Credentials'a ardından, Create Credentials'a hemen ardından ise OAuth client ID butonlarına tıklıyoruz.
    Screenshot_121
  4. Gerekli ayarları yaptıktan sonra, Authorized redirect URIs kısmına yetkilendirmek istediğimiz linkleri yazıyoruz. Burası en önemli kısımlardan birisi olduğu için dikkat ediniz :)
    Screenshot_123
  5. Ve :tada: API key ve secret keylerimiz hazır. Bunlar bize ileride lazım olacağı için sağa sola bir kenara kaydedelim.
    Screenshot_122
  6. Ayarlamaları yapmaya devam edelim, soldaki menüden OAuth consent screen bölümüne geçelim.
    Screenshot_124
  7. Burayı da geçtikten sonra, önümüze gelen paneldeki gerekli olan inputları dolduralım.
    Scopes kısmına geldiğimizde, kullanıcının hangi verilerini çekmek istediğimiz bilgisini isteyecek. Bana email ve bazı kişisel bilgileri lazım olduğu için görseldeki gibi işaretliyorum.
    Screenshot_125
  8. Hepsini tamamladıktan sonra, uygulamayı yayına alalım ve google cloud ile işimizi tamamen bitirelim!
    Screenshot_126

Backendin kurulması

Yazıda da belirttiğim gibi bu projede nestjs kullanacağım.
Yeni bir proje açıp ardından hemen konsola geçiyorum.

Ardından nest-cli kurulu değilse onu kuruyorum.

npm install -g @nestjs/cli

CLI'i kullanarak yeni bir nest uygulaması yaratıyorum.

nest new .

Şimdi de passport-google-oauth20 kütüphanesini yükleyelim.

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

Objeleri DTO'a çevirmeye işe yarayan plainToClass fonksiyonunu kullanmak için class-transformer kütüphanesini kuralım.

npm install class-transformer --save  

Vee projemiz hazır..
Screenshot_127

Hemen ardından projeyi ayağa kaldırıyorum.

npm run start:dev

Screenshot_128
Bu ekranı görüyorsanız sıkıntı yok demektir, devam edebilirsiniz :)


Typeorm İle Mongodb'nin Entegre Edilmesi

Projeye typeorm ve mongodb paketlerini yüklüyorum.

npm install @nestjs/typeorm typeorm [email protected] --save

** [email protected] > üzeri sürümlerinde gerçekleşen büyük değişikliklerden dolayı, "Right-hand side of 'instanceof' is not an object" gibi oluşabilecek hataları önlemek için [email protected] sürümüne düşürüldü. **

//app.module.ts
...
import { TypeOrmModule } from '@nestjs/typeorm';
...
//app.module.ts
...
imports: [
    TypeOrmModule.forRoot({
      type: 'mongodb',
      url: 'mongodb+srv://username:[email protected]/databasename?retryWrites=true&w=majority',
      authSource: 'admin',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      ssl: true,
      useUnifiedTopology: true,
      useNewUrlParser: true,
      synchronize: true,
      logging: true,
    }),
  ],
...

Kullanıcı Modülünün Oluşturulması

Terminalden kök dizine geliyoruz ve hemen ardından aşağıdaki kodu çalıştırıyoruz.

nest g resource user
//REST API [x]
//CRUD [x]

Yeni eklenen dosyalar;
Screenshot_129

Entity ve DTO'nun düzenlenmesi

Adı, soyadı ve email adresinden oluşan bir kullanıcı tablosu hazırlayacağım.

//user.entity.ts
import { Column, Entity, ObjectID, ObjectIdColumn } from 'typeorm';

@Entity('User')
export class User {
  @ObjectIdColumn()
  id: ObjectID;

  @Column()
  name: string;

  @Column()
  surname: string;

  @Column({ unique: true })
  email: string;
}
//create-user.dto
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class CreateUserDto {
  @Expose()
  public name: string;

  @Expose()
  public surname: string;

  @Expose()
  public email: string;
}
//user.module.ts
...
imports: [TypeOrmModule.forFeature([User])],
...
//user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
  ) {}
  async create(createUserDto: CreateUserDto) {
    return await this.userRepository.create(createUserDto);
  }

  async findOne(data: object) {
    return await this.userRepository.findOne(data);
  }
}
//user/guards/google-auth-guard.ts
import { HttpException, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
  constructor() {
    super();
  }

  handleRequest(err: any, user: any, info: any, context: any, status: any) {
    if (err || !user) {
      throw new HttpException(err.message, err.status);
    }
    return user;
  }
}
//user/strategies/google.strategy.ts
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { UserService } from '../user.service';
import { PassportStrategy } from '@nestjs/passport';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private userService: UserService) {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_SECRET,
      callbackURL: `http://localhost:3000/auth/google`,
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ): Promise<any> {
    const { name, emails } = profile;
    const email = emails[0].value;

    const newUser = plainToClass(CreateUserDto, {
      email,
      name: name.givenName,
      surname: name.familyName,
    });

    const foundedUser = await this.userService.findOne({ email });

    if (foundedUser) return done(null, foundedUser);
    else {
      const savedUser = await this.userService.create(newUser);
      return done(null, savedUser);
    }
  }
}

clientId: Yukarıda google'dan aldığımız clientId,
clientSecret: Yukarıda google'dan aldığımız clientSecret,
callbackURL: Authorized redirect URI kısmına tanımladığımız URL,
scoped: Kullanıcının çekmek istediğimiz verileri..

Son oluşturduğumuz google.strategy.ts dosyasını, user.module.ts'e providers olarak dahil edelim.

//user.module.ts
...
providers: [UserService, GoogleStrategy],
...

Ve tüm işlemler tamamlandı!


Google Oauth Servisinin Kullanılması

Şimdi de bu servisi nasıl kullanabiliriz gelin ona bakalım.

localhost:3000/auth/google adresine istek attığımızda, login değilsek direkt olarak google auth panel gelecektir ve otomatik olarak callbackUrl'de tanımladığımız linke yönlenecektir. Tüm bu yönlendirme işlemlerini kullandığımız passport-google-oauth20 kütüphanesini bizim yerimize yapıyor.

callbackUrl'e gelirken code parametresiyle dönecektir. Tabi bunları kendi içinde yaptığı için uzaktan bakınca servisin nasıl çalıştığını anlamak bir hayli zorlaşıyor.

Gelin bu zorluğu biraz aşmaya çalışalım.
localhost:3000/auth/google adresine istek attığımızda bizi hesap seçmemiz için başka bir linke yönlendiriyor. Bu yönlendiğimiz sayfayı manuel olarak kendimiz yazacak olsaydık;

https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&response_type=code&redirect_uri=http://localhost:3000/auth/google&client_id=clientId

clientId: Yukarıda google'dan aldığımız clientId.

Yukarıdaki linke query olarak geçirdiğimiz redirect_uri'nin backend ve google cloud'da Authorized redirect URI kısmıyla eşleşiyor olduğuna dikkat edelim.
Atılan bu istekte, önce hesap seçilecek ardında bizi redirect_uri'e code parametresini ekleyerek otomatik olarak yönlendirecektir ve direkt olarak code'i çözüp bilgileri database'e kaydedip, kaydettiği verileri tekrar bize dönecektir.


Son

Okuduğunuz için çok teşekkürler🚀
Kucak dolusu sevgilerle!


Meraklısı İçin Kaynakça

Using OAuth 2.0 to Access Google APIs | Google Identity
MongoDB
Cast entity to dto | Newbedev
So based on Jesse’s awesome answer I ended up creating the DTO using @Exclude() and @Expose() to remove all but exposed properties: import { IsString, IsEmail }
How to add a FREE MongoDB database to your NestJS API with TypeORM
Need a free and performant database for your NestJS app? Learn how you can easily integrate Azure Cosmos DB using TypeORM and the MongoDB driver with this hands-on tutorial.
Tolga Çağlayan

Tolga Çağlayan

En tehlikeli kelime nedir Olric? -Ama’dır efendim bana göre. Neden Olric? -Önceden söylenen her söylemi veya kelimeyi öldürür! Mesela, seni seviyorum ama. gibi.
Anonim