前編の続きです。
認証/認可がイマイチわかっていなかったので、ChatGPTに相談しながら実装してみました。
後編の今回はフロント側の実装を行います。
今回実装する認証の流れ
今回実装する認証は、以下のような流れを想定しています。
- フロント側でメールアドレスやGoogleアカウントなどでログインを行う
- Firebase AuthenticationがIDトークン(JWT)を発行する
- NestJS側でIDトークンを検証する
- 検証がOKなら続きの処理を行う
- 処理結果をレスポンスとして返し、フロントは画面に結果を表示する
🛠 実装のステップ
▪️前編(前回)
前編では先ほどの処理の流れの3、4を実装します。
バックエンド側での「認証」機能を作成します。
- Firebaseの環境構築
- NestJSでバックエンドを構築
- AuthGuard の作成(IDトークンのJWT検証)
a. これで「認証」を行う
▪️後編(今回はこっち)
後編はフロントエンド側でログインし「IDトークン」を取得し、結果を表示します。
- フロントでの ID トークン取得ユーティリティ作成
a. 「認証」はバックエンド側で行いますが、利便性のためフロントでも検証 - フロント画面でのログイン表示
フロントの環境構築
まずはフロントの環境を構築します!ビルドツールはVite、フロントのフレームワークはReactを使います。
npm create vite@latest firebase-auth-demo --template react-ts
cd firebase-auth-demo
npm install
新規登録・ログイン機能を作る
次は新規登録・ログイン機能を作ります。
authインスタンスをエクスポート
まずはFirebaseからauth
インスタンスをエクスポートします。
auth
インスタンスは、Firebaseの認証機能を使うためのものです。設定したFirebaseプロジェクトへのアクセスの窓口として機能します。
このインスタンスごとに、現在ログインしているユーザーや設定を保持しています。
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
// Firebase コンソールで作成したプロジェクトの設定値
};
// Firebase アプリを初期化
const app = initializeApp(firebaseConfig);
// Firebase Auth をエクスポート
export const auth = getAuth(app);
画面を作る
ReactのApp.tsx
でログイン画面を作ります。
Firebaseは必要な機能をいい感じに提供してくれるので、新規登録はcreateUserWithEmailAndPassword()
、ログインはsignInWithEmailAndPassword()
関数を使うと簡単に認証を行ってくれます。
この関数を使うことで、Firebase Authenticationが内部的にIDトークン(JWT)を発行し、クライアントに渡します。
import { useEffect, useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
import { auth } from "./lib/firebase";
function App() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const register = () => {
createUserWithEmailAndPassword(auth, email, password).catch(console.error);
};
const login = () => {
signInWithEmailAndPassword(auth, email, password).catch(console.error);
};
return (
<>
<input placeholder="Email" onChange={(e) => setEmail(e.target.value)} />
<input placeholder="Password" type="password" onChange={(e) => setPassword(e.target.value)} />
<div style={{ marginTop: "1rem" }}>
<button onClick={register}>新規登録</button>
<button onClick={login}>ログイン</button>
</div>
</>
)
}
export default App;
認証OK後の画面を作る
新規登録・ログインが完了すると(認証が完了すると)、Firebase Authenticationからuser
インスタンスが取得できます。
このuser
インスタンスがあるか?を監視することで、新規登録・ログイン後に画面を変化させます。
この監視の処理もFirebase側でいい感じのonAuthStateChanged()
関数を用意してくれています。これをuseEffect
で画面のレンダリング後に1回だけ実行します。
const [user, setUser] = useState<any>(null);
useEffect(() => {
// ① リスナー登録して、変更を監視
const unsub = onAuthStateChanged(auth, (user) => {
setUser(user);// ② 認証状態が変わったら state を更新
});
// ③ クリーンアップ:コンポーネントが消えたらリスナー解除
return () => unsub();
}, []);
// 省略
return (
<div>
{user ? (
<>
{/* 認証が完了したらuserがセットされるので、こっちの画面が表示される */}
<p>Welcome, {user.email}</p>
<button onClick={logout}>Logout</button>
</>
) : (
<>
{/* userがなければログイン・新規登録画面を表示する */}
</>
)}
</div>
)
バックエンドで作ったAPIを呼び出す
ここまではフロント側での認証でした。
前回バックエンド側で作ったAPIを呼び出してみます。
前回のおさらい
前回バックエンドでは/hello
というエンドポイントを作りました。
そこではHTTPヘッダーにIDトークンを渡して、そのIDトークンが正しいか?を検証していました。
APIを呼び出してみよう
API呼び出し処理
「Helloを呼び出す」ボタンを押すとgetHello()
関数が呼ばれます。
この関数の中では、まずauth.currentUser
から現在のユーザーを取得します。
ログインまたは新規登録している場合はここにはユーザーの情報が入ります。万一入ってない場合はエラーにします。
user.getIdToken()
で認証した後に発行されたIDトークンを取得できます。
引数のtrue
はキャッシュを破棄するかどうかですが、ここでは説明は省きます。
トークンを取得できたら、HTTPヘッダーにトークンを設定してリクエストを送ります。これでバックエンドにリクエストを送れました!バックエンド側の処理は前編で実装済みです。
レスポンスを表示
リクエストが正常に処理されると、res
にバックエンドから「Hello World!」が返されます。
これをmessage
に格納して画面に表示させます。
import { auth } from './firebase';
const [message, setMessage] = useState<string>(null);
const getHello = async () => {
// ログイン中のユーザーを取得する
const user = auth.currentUser;
if (!user) {
throw new Error('ログインしてないよ!');
}
// IDトークンを取得してHTTPヘッダーに設定
const token = await user.getIdToken(true);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const {data} = await res.json();
setMessage(data);
}
return (
{/*好きなところにボタンを設置*/}
<button onClick={getHello}>Helloを呼び出す</button>
// getHello()した後のレスポンスを表示
{message &&
<p>{message}</p>
}
)
これでフロント側の実装が完了しました!
まとめ
前編/後編にわけて認証の実装をしました。
繰り返しになりますが、認証とは そのユーザーが本人か? を検証する仕組みです。
認証の方法はいくつかあると思いますが、今回はベーシックな IDトークン(JWT) を使った認証を実装して簡単な流れを確認しました。
- 新規登録・ログインを行う
- ユーザーの登録/認証を行い、IDトークンを発行する
- 取得したIDトークンをHTTTPヘッダーに設定してAPIを呼び出す
- バックエンド側で渡されたIDトークンを検証して、ユーザーが本人か?を確認する
- 本人ならAPIの処理を行う。本人でないならエラーを返す
こんな感じで、「認証」をして安全なアプリを構築するんですね!
今回は「認証」を行いましたが、今度は権限(一般ユーザー/管理者 など)を検証する「認可」についても勉強してみようと思います。