フロントエンドエンジニアとして働いていながら、認証/認可がイマイチわかっていなかった私。
Chat GPTに相談しながらReact × NestJS × FirebaseAuthで簡単な認証/認可の流れを実装しながら確認しました。
- この記事のコードの一部はChat GPTが書いたものが含まれています
- 文章の一部もChat GPTに相談して解説してもらったものが含まれています
- 記事の全体的な構成は、筆者と同じように認証/認可がよくわからない人向けに、筆者が考えて構成したものです
認証とは
認証とは、「ユーザーが本人であること」を証明する仕組みです。
今回はFirebase Authenticationを使ってその"証明書”となる IDトークンを発行し、Nest.js側でそのトークンを検証します。
認証はほとんどのアプリで実装されている機能です。
たとえばQiitaでもマイページの表示、投稿の編集などは、ログインしているユーザー本人だけが操作できるようにする必要がありますよね。
この「本人確認」の仕組みが認証です。
今回実装する認証の流れ
今回実装する認証は、以下のような流れを想定しています。
- フロント側でメールアドレスやGoogleアカウントなどでログインを行う
- Firebase AuthenticationがIDトークン(JWT)を発行する
- NestJS側でIDトークンを検証する
- 検証がOKなら続きの処理を行う
- 処理結果をレスポンスとして返し、フロントは画面に結果を表示する
🛠 実装のステップ
▪️前編(今回はここまで)
前編では先ほどの処理の流れの3、4を実装します。
バックエンド側での「認証」機能を作成します。
- Firebaseの環境構築
- NestJSでバックエンドを構築
- AuthGuard の作成(IDトークンのJWT検証)
a. これで「認証」を行う
▪️後編
後編はフロントエンド側でログインし「IDトークン」を取得し、結果を表示します。
- フロントでの ID トークン取得ユーティリティ作成
a. 「認証」はバックエンド側で行いますが、利便性のためフロントでも検証 - フロント画面でのログイン表示
1. Firebaseの環境構築
まずはFirebaseの環境を構築します!
セットアップ
Firebaseをプロジェクトにインストールします。
npm install firebase
Firebaseコンソールにログインしてプロジェクトを構築します。コンソール画面から「Firebaseプロジェクトを作成する」ボタンを押下します。
基本的には画面に沿って設定するだけですが、選択肢がある場合は以下の通り設定します。
- 今回はWebアプリとしてプロジェクトを作ります
- Firebase Hostingも設定
- すでにViteでアプリを構築済みなので「Use an existing project」を選びます
- デプロイ用のディレクトリは「dist」に設定します(Viteでビルドした時の初期設定に合わせる)
デプロイしてみる
セットアップができたらFirebaseをデプロイしてみます。
# アプリをビルドします
npm run build
# Firebaseをデプロイします
firebase deploy
コンソールにHosting URLが表示されるので、クリックして画面が表示されたら基本的な構築は完了です!🥳
2. NestJSでバックエンドを構築
APIで送られたリクエストが「本人かどうか」をバックエンド側で検証するロジックを構築します。
具体的には、APIが呼ばれたときにNestJSのAuthGuardという仕組みを使って、リクエストに含まれるIDトークン(JWT)を検証します。
検証がOKなら「認証済み」として処理を進めます。逆にNGの場合は401 Unauthorized
エラーを返し、不正なアクセスをブロックします。
バックエンドのプロジェクトを作成
NestJSのプロジェクトを作成します。
# Nest CLIがなければインストール
npm install -g @nestjs/cli
# 新規プロジェクト作成(backendフォルダに)
nest new backend
cd backend
npm install
Firebase Admin SDKのセットアップ
まずはFirebaseのサービスアカウントキーをFirebaseコンソールから取得します。
- Firebaseコンソールを表示
- 歯車マークを押下
- 「プロジェクトの設定」
- 「サービスアカウント」タグ
- 「新しい秘密鍵を生成」
- jsonファイルをダウンロード
サービスアカウントキーは秘密鍵です。これはGithubの公開リポジトリなど、不特定多数から見られないように管理します。
取得したサービスアカウントキーを保存
取得したサービスアカウントキーをプロジェクトルート/src/var/secrets/
ディレクトリを作成して保存します。
/var/secrest/
ディレクトリは.gitignore
に追加しておきましょう。
環境変数を追加
backend
ディレクトリに.env
ファイルを追加します。
この.env
ファイルに環境変数を追加し、先ほどのサービスアカウントキーの絶対パスを渡します。
GOOGLE_APPLICATION_CREDENTIALS=絶対パス
環境変数を扱う便利ライブラリ
cd backend # バックエンドプロジェクトのルートへ
npm install @nestjs/config
backend側のapp.module.ts
でnestjs/config
を読み込む
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
],
// そのほかの設定
})
export class AppModule {}
Firebase Admin SDKを設定する
Firebase Adminをインストールします。
Firebase AdminはFirebaseの“サーバーサイド専用”SDKで、今回作る認証のほか、データベースの機能などが使えます。
npm install firebase-admin
初期設定
backend
ディレクトリ直下にfirebase-admin.ts
を作成します。
ここで先ほど環境変数に設定したサービスアカウントキーの登録処理が必要ですが、nestjs/config
を使うと自動的に読み込んでくれます。
import * as admin from 'firebase-admin';
// nestjs/configがあれば.envファイルから勝手に環境変数を読み込んでくれる
admin.initializeApp();
3. 認証機能の作成
いよいよ認証機能を作ります!
Nest.jsにはCanActivate
というインターフェースがあります。このインターフェースは「このリクエストを処理していいかどうか」を判定する仕組みです。
これを実装し、Nest.jsの仕組みであるDI(依存性注入)して使います。
IDを検証する関数を作る
IDを検証する仕組み自体はfirebase-admin
にすでにあります。token
を受け取って検証するだけの関数です。
import * as admin from 'firebase-admin';
// 初期化
admin.initializeApp();
/**
* トークン(JWD)を検証する
* @param token
*/
export const verifyIdToken = (token: string) =>
admin.auth().verifyIdToken(token);
AuthGuardを作る
ここのポイントは3つです。
@Injectable()
CanActivate
- HTTPヘッダーからBearerトークンを取得して検証
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { verifyIdToken } from './firebase-admin';
@Injectable()
export class FirebaseAuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
// HTTPリクエストからauthorizationヘッダーを取得して検証する
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization || '';
const token = authHeader.replace('Bearer ', '');
if (!token) throw new UnauthorizedException('No token');
try {
// Firebase Adminで検証
const decoded = await verifyIdToken(token);
req.user = decoded;
return true;
} catch {
// 検証してNGの場合はエラーを投げる
throw new UnauthorizedException('Invalid token');
}
}
}
ポイント①@Injectable()
先ほども書いた、NestJSでDIをするための設定です。
これを書いたものはプロバイダーと呼ばれ、他のクラスなどで使いまわせます。
ポイント②CanActivate
こちらも先ほど書いたNestJSで認証機能を使うためのインターフェースです。implements
して使います。
ポイント③HTTPヘッダーからBearerトークンを取得して検証
今回は、HTTPリクエストのAuthorizationヘッダーを使用してトークンを送信/受信します。
Authorization
にBearer(保持者)とそのIDトークンを設定してHTTPリクエストを送信し、そのヘッダーを解析してバックエンド側で認証を行います。
AuthGuardを使う
作ったAuthGuardを使ってみます。
モジュールに登録
@Injectable()
を付けたクラスはモジュールに登録することで使えるようになります。
以下のようにprovider
に渡してあげます。
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true}),
],
controllers: [AppController],
// ここに登録
providers: [AppService, FirebaseAuthGuard],
})
APIリクエストを受け取って検証
フロント側からのリクエストを受け取るコントローラーでAuthGuardを使います。
@UseGuards()
はガードを使うためのNestJSの仕組みです。ここに先ほど作ったガードを渡すと、リクエストが来た時にIDトークンを検証します。
import { Controller, Get, UseGuards } from '@nestjs/common';
import { FirebaseAuthGuard } from './firebase-auth.guard';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
// UseGuardsで使いたいGuardを登録する
@UseGuards(FirebaseAuthGuard)
@Get("/hello")
getHello(): {data: string} {
return {data: 'Hello World!'};
}
}
これでバックエンド側の実装は完了です!
まとめ
送られたHTTPリクエストのIDトークンを検証する、という機能を実装しました。
後編ではフロント側のログイン機能を実装します。