ども!今回は、改めてReactでFirebaseのリダイレクトログインについてまとめました。社内システムでは、すべてがログインユーザである必要があるので、ログインしていない場合は自動でログインするようにしています。BFFでフロントとバックを分離するためのステップ1
最初の雑談
ども!3日ぶりに外に出たら、雪が積もっていてびっくりした龍ちゃんです。まだ残ってると思っていませんでしたが、かっちこちに凍っていて滑りそうになりました。
さて、今回はフロントエンドの話になります。過去記事で、フロントのみで構築していたアプリケーションをBFFを意識して分割していこうとしています。今回は実践編その1になります。
改めてフロントエンドをFirebaseのクライアントとしてセットアップしていきたいと思います。今回の記事でわかることは以下になります。
- ユーザーがログインしていない場合は、Firebaseを用いてGoogle認証画面にリダイレクト
- axiosを用いて自動でidTokenをHeaderに付与
それでは内容に移っていきます。
お願い!Firefoxでは、cookieがブロックされるため無限リダイレクトが発生します。ChromeかEdgeで検証してください。この回避方法に関しては、また調べます。
事前準備
事前準備として、Firebaseのアプリケーション設定と環境変数を保存しておきます。環境としては、React +Viteで環境を作成しています。Firebase Console側の設定は以下の記事を参考にしてもらえればと思います。
コンソールから取得した情報は、環境変数として保存します。
VITE_FB_APIKEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VITE_FB_AUTHDOMAIN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VITE_FB_PROJECT_ID="nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VITE_FB_STORAGE_BUCKET="nexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
VITE_FB_MESSAGEING_SENDER_ID="xxxxxxxxxxxxx"
VITE_FB_APP_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
あとは、開発に使用するモジュールのインストールをしておきます。
npm install firebase axios swr
それでは、構築に入っていきます。
FirebaseのログインとidTokenの取り回し
今回目指す構築の全体像について共有していきます。フロントエンドの実装としては、Firebase Autenticationで認証して、idToken
をaxios
のheader[”google-certification”]
として付与して送信します。今回はGoogleの認証のみを利用して、認可は実装しないのでアクセストークンではなくidTokenを利用します。
段階含めて設定していきたいと思います。今回のディレクトリ構造は以下になります。
src/
├── auth
│ └── authFirebase.ts
├── hooks
│ └── useAuth.ts
├── pages
│ ├── layout
│ │ └── ParentLayout.tsx
│ └── TopPage.ts
├── utilities
│ ├── AxiosConfig.ts
│ └── FirebaseInit.ts
├── App.css
├── App.tsx
├── index.css
├── main.tsx
└── vite-env.d.ts
以下のステップで解説していきます。
- Firebaseの初期設定とFirebase周りのhooks化
- Firebaseで自動リダイレクトログインの作成
- axiosのintersepterでheaderにidTokenを挿入する
それでは、解説していきます。
Firebaseの初期設定とFirebase周りのhooks化
まずは、Firebase SDKをローカルで使用することができるように初期設定を行っていきます。auth
配下にauthFirebase.ts
に以下の内容をコピーしてください。
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
// Initialize Firebase
const app = initializeApp({
apiKey: import.meta.env.VITE_FB_APIKEY,
authDomain: import.meta.env.VITE_FB_AUTHDOMAIN,
projectId: import.meta.env.VITE_FB_PROJECT_ID,
storageBucket: import.meta.env.VITE_FB_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FB_MESSAGEING_SENDER_ID,
appId: import.meta.env.VITE_FB_APP_ID,
});
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
hd: 'sios.com',
});
export { auth, provider };
こちらでは、Firebase SDKの初期化とプロバイダーの設定を行っています。GoogleAuthProvider
に追加設定を行うことでログイン周りの追加で制約を縛ることができます。具体的なパラメータはこちらにあります。
こちらでエクスポートしたものを利用することで、Firebaseの機能を扱うことができます。次はhooks
配下のuseAuth.ts
に以下の内容をコピーしてください。
import { signInWithRedirect, signOut } from 'firebase/auth';
import useSWR from 'swr';
import { auth, provider } from '@/auth/authFirebase';
import { firebaseAuthType } from '@/types/firebaseAuthType';
export const useAuthAction = () => {
const signInAction = () => {
signInWithRedirect(auth, provider).catch((err) => {
alert(err);
});
};
const singOutAction = () => {
signOut(auth);
};
return { signInAction, singOutAction };
};
export const useAuthenticated = () => {
const uid: string = auth.currentUser ? auth.currentUser.uid : '';
const { data: idToken, isLoading: isLoadingIDToken } = useSWR('idToken', async () => {
if (auth.currentUser === null) return '';
const token = await auth.currentUser.getIdToken();
return token;
});
return { idToken, isLoadingIDToken, uid };
};
こちらのhooks
では、以下の二つがあります。
- ログイン周りのアクション
- ログイン後にユーザー情報にアクセスするアクション
idTokenの取得などもまとめて処理として入れています。idTokenの取得では、非同期処理が走るためSWRを用いて、情報の状態を管理しています。
Firebaseで自動リダイレクトログイン
ログイン処理はutilities
のFirebaseInit.tsx
にまとめてあります。
import { useEffect, useState } from 'react';
import { onAuthStateChanged, User } from 'firebase/auth';
import { auth } from '@/auth/authFirebase';
import { useAuthAction } from '@/hooks/useAuth';
type Props = {
children: React.ReactNode;
};
export const FirebaseInit = (props: Props) => {
const { children } = props;
const { signInAction } = useAuthAction();
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (getAuthUser: User | null) => {
if (getAuthUser == null) signInAction();
setLoading(false);
});
return unsubscribe;
});
if (loading || auth.currentUser == null) return <div>loading</div>;
return <>{children}</>;
};
こちらでは、先ほど作成したuseAuthAction()
を使用してログイン処理を行っています。ユーザーの状態を監視して、ログインユーザーがいない場合はログイン処理に飛ばしています。また、ログインユーザーがいるまでLoading画面を表示しています。
axiosのinterceptorでheaderにidTokenを挿入する
ここでは、フロントとバックの通信をセキュアに担保するためにトークンのやり取りを自動でHeaderに挿入しています。すべてのAPI通信において、Headerを挿入することで対応漏れを防ぐことができます。なぜ?トークンでの運用が必要かについては以下の記事でまとめています。
それでは実際のコードです。こちらの処理はutilities
のAxiosConfig.tsx
にまとめてあります。
import React, { useEffect, useState } from 'react';
import { useErrorBoundary } from 'react-error-boundary';
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { useAuthenticated } from '@/hooks/useAuth';
export const axiosClient = axios.create({
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
type Props = {
children: React.ReactNode;
};
export const AxiosConfig = (props: Props) => {
const { children } = props;
const { showBoundary } = useErrorBoundary();
const { idToken } = useAuthenticated();
const [isTokenSet, setIsTokenSet] = useState<boolean>(false);
useEffect(() => {
const requestInterceptor = axiosClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
config.headers['google-certification'] = `${idToken}`;
return config;
},
(error: AxiosError) => showBoundary(error),
);
const responseInterceptor = axiosClient.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
if (idToken) setIsTokenSet(true);
return () => {
axiosClient.interceptors.response.eject(responseInterceptor);
axiosClient.interceptors.request.eject(requestInterceptor);
};
}, [idToken]);
if (!isTokenSet) return <div>loading</div>;
return <>{children}</>;
};
useEffect
内の処理が実際に対応している内容になります。今回は、headers[’google-certification']
に情報を付与して送信しています。Authorizationに送付して送ろうかと悩んでいたのですが、ざっくりと調べた限り認可用のAccessToken
を付与するほうが正しいようなので、新しく作りました。
重要な点としては、idTokenがセットされるまで子要素を描画しない点です。useEffectの起動タイミングとidTokenの取得が競合して、Headerが付与されていないまま送付されるとエラーになるので、描画をisTokenSet
でidToken
のセットと同期をとっています。
使用方法としては、axiosを使用したい場面で上部でエクスポートされているaxiosClient
を使用して処理を記述するだけです。axiosで書いたら機能しないので注意してください。
全体の描画ファイル
こちらでは、全体の設定ファイルを一つのファイルとしてまとめていきます。ファイルとしては、pages
内のlayout
になります。概念図を以下に出しておきます。
FirebaseInit
が一番外にある必要があります。これは、ログイン処理が済んでいる状態じゃないとidTokenの取得ができないためです。また、描画要素内で情報の取得を行うため描画要素とFirebaseInit
の間にAxiosConfig
を挟んでいます。
実際のコードです。
import { AxiosConfig } from '@/utilities/AxiosConfig';
import { FirebaseInit } from '@/utilities/FirebaseInit';
type Props = {
children: React.ReactNode;
};
export const ParentLayout = (props: Props) => {
const { children } = props;
return (
<>
<section>
<main>
<FirebaseInit>
<AxiosConfig>{children}</AxiosConfig>
</FirebaseInit>
</main>
</section>
</>
);
};
最終的に以下のように使用すれば、すべて機能します。
export const App = ()=>{
return (
<ParentLayout>
<適当なコンポーネント/>
</ParentLayout>
);
【適当なコンポーネント】部分で、axiosClient
を使用して通信することでログイン処理とAPI通信部分に自動でHeaderが付与されます。検証のために適当なコンポーネントを置いておきます。
import { axiosClient } from '@/utilities/AxiosConfig';
export const TopPage = () => {
axiosClient.get('/api');
return <div className="bg-white">TopPage</div>;
};
終わり
お疲れ様です。Firebaseでアクセストークンを取得しようとして挫折しました。まだまだ勉強不足を実感させられます。Google側で認可を作成するか、BFFでカスタムトークンを作成して認可をするべきなのか?という疑問が発生します。また、OIDCについて勉強してきましょう。
とりあえず、idTokenがあることで誰からのアクセスか?という特定と担保はできます。実際にトークン検証をnest.jsで実装してみました。header上から取得して不正なidTokenははじいてくれます。
ではまた!
Twitterのほうもよろしくお願いします。