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管理に利用するということを意識されるとより安全に認証機能を実現できるようになると思います。