NestJSでgoogle oauth認証を実装する
準備
新しい nest プロジェクトを nest コマンドで作ってそこで作業します
nest new nest-oauth
できたら、そこに移動して起動するかどうか試してみましょう
cd nest-oauth
npm run start:dev
http://localhost:3000 にアクセスして「Hello World!」と出れば準備 OK です
oauth2.0 クライアント ID を取得する
GCP のプロジェクトを作り、「認証情報」→「認証情報を作成」→「oauth クライアント ID」を選択し、以下の内容で作成します
- アプリケーションの種類:ウェブアプリケーション
- 名前:任意
- 承認済みの Javascript 生成元:http://localhost:3000
- 承認済みのリダイレクト URI:http://localhost:3000/google/redirect
必要なパッケージのインストール
passportという express で有名な認証ライブラリを使います。passport は色々な認証方式(strategy)に対応しています。実装したい認証方式に対応したライブラリをインストールすることで、その認証方式を簡単に実装することができます。今回は google oauth に対応したいので、それ用のライブラリをインストールします。
npm i --save @nestjs/passport passport passport-google-oauth20 @nestjs/config dotenv
npm i -D @types/passport-google-oauth20
nest cli コマンドで必要なファイルを生成
今回は google フォルダを作ってその中に google oauth 関連のファイルを詰め込みたいと思います
nest g res google # controllerとserviceとmoduleを作成
passport を使って認証部分を実装する
今回は passport を用いた google oauth に関する設定ファイルとしてgoogle.strategy.tsを以下のような内容で作成します。
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { config } from 'dotenv';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
config(); // .envファイルを使えるようにする
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
// Passportのstrategyに関する設定
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL,
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
accessToken,
};
done(null, user);
}
}
.envファイルを作ります
GOOGLE_CLIENT_ID= <your-client-id>
GOOGLE_CLIENT_SECRET= <your-client-secret>
CALLBACK_URL=http://localhost:3000/google/redirect
google.controller.tsで必要なルートを設定します。
AuthGuardを import して、ハンドラーを guard し、そこにリクエストが行くことで、google oatuh 認証フローが開始します。
import { AuthGuard } from '@nestjs/passport';
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { GoogleService } from './google.service';
@Controller('google')
export class GoogleController {
constructor(private readonly googleService: GoogleService) {}
// ここにリクエストがいくことでoauth認証フローのスタート
@Get()
@UseGuards(AuthGuard('google'))
async googleAuth(@Request() req) {}
// 認証フローが終了し、アクセストークンを取得したら、ここにリダイレクトされる
@Get('redirect')
@UseGuards(AuthGuard('google'))
googleAuthRedirect(@Request() req) {
// この時点でreq.userに上のほうで定義したvalidateで抜き出した認証情報が入っている(名前、メールアドレス、画像など)
// 具体的な処理はserviceにやらせる
return this.googleService.googleLogin(req);
}
}
google.service.tsの中を書いていきます。
import { Injectable } from '@nestjs/common';
@Injectable()
export class GoogleService {
googleLogin(req) {
if (!req.user) {
return 'No user from google';
}
return {
message: 'User information from google',
user: req.user,
};
}
}
最後に、DI のためにgoogle.module.tsをごにょごにょして、それをapp.module.tsで import すれば OK です。
import { Module } from '@nestjs/common';
import { GoogleService } from './google.service';
import { GoogleController } from './google.controller';
import { GoogleStrategy } from './google.strategy';
@Module({
controllers: [GoogleController],
providers: [GoogleService, GoogleStrategy],
})
export class GoogleModule {}
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GoogleModule } from './google/google.module';
@Module({
imports: [GoogleModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
テスト
npm run start:dev
この状態で、http://localhost:3000/google にアクセスして、google アカウントでログイン後、http://localhost:3000/google/redirect にリダイレクトされ、ユーザー情報が見れれば OK です。
参考
Discussion
userの定義で最終的にユーザーIDを取得していないことが少し気になってしまいました。
メールアドレスをID管理のキーに利用することはアンチパターンです(例えGoogleアカウントのGmailであってもです)。万が一メールアドレスが変わったり新たなメールアドレスが付与された場合に連携がうまくできなくなる可能性があります。
認証用途であれば
scope=openidを利用してユーザーIDを取得してその後のID管理に利用するということを意識されるとより安全に認証機能を実現できるようになると思います。