Nuxt.js で Firebase Authentication を使用した Web アプリケーションの構築を行ったので、その際の認証周りの設計をまとめておきます。
認証状態を持つのは Firebase のみとし、Nuxt.js 側では認証状態を持たないようにしました。つまり、Store を用いたログイン状態の管理等は行わないということです。これは、認証状態の管理コストが二重になるのを避けるためです。認証状態の同期(commit
)や、不整合起きた場合のハンドリングなどが煩雑にならないようにこのような設計方針としました。
firebase.auth().onAuthStateChanged(user => {})
でログインされているユーザーを取得することができます。
ログアウト状態になった場合も、このオブザーバーでハンドリングを行います。オブザーバーのラッパー関数を作成して、ログイン時に、オブザーバーの登録を行います。
/* plugins/Firebase.ts */
/*
オブザーバーのラッパー関数
ログインしたときと、ログアウトしたときの処理を、呼び出し元で定義する。
*/
export const registerFirebaseAuthStateObserver = (signinCallback: Function, signoutCallback: Function) => {
auth.onAuthStateChanged(user => {
if (user) {
// ログインされたとき
console.log('=== SIGNIN');
signinCallback(user);
} else {
// ログアウトされたとき
console.log('=== SIGNOUT');
signoutCallback();
}
});
};
/* plugins/Firebase.ts */
/**
* アプリケーション起動時に、Firebase オブザーバーを登録し、
* ログイン状態を監視する
* セッションが切れたら、自動でログイン画面に replace される
*/
export default ({ app }: any) => { // any は Context という型だけど今回は割愛
const signinCallback = () => {
console.log('=== Signin');
};
const signoutCallback = () => {
console.log('=== Signout');
app.router.replace('/signin');
};
registerFirebaseAuthStateObserver(signinCallback, signoutCallback);
};
このようにオブザーバーを登録しておくことで、Firebase のログアウトメソッド firebase.auth().signOut()
を呼び出して認証状態がログアウト状態に変化したときも、onAuthStateChanged
が発火し、共通的に処理を行わせることができます。
Firebase Authentication でログインしているユーザー情報を取得する方法は2種類あります。先に書いた firebase.auth().onAuthStateChanged(user => {})
オブザーバーを使う方法と、firebase.auth().currentUser
を使う方法です。後者は、タイミングによって、 Auth
オブジェクトが初期化されていない場合があり、公式ドキュメントでは、前者のオブザーバーを使ってユーザーを取得する方法が推奨されています。
firebase.auth().currentUserc だと、 currentUsercZ が null になってしまう場合があります。📙Firebase 公式ドキュメントahttps://firebase.google.com/docs/auth/web/manage-users?hl=ja#get_the_currently_signed-in_user
とはいうものの、 user
情報は Store
では管理しない設計方針なので、使いたいタイミングで、 user
オブジェクトをうまく取得してくる必要があります。 user
オブジェクトはコールバック引数として渡ってくるので、少し扱いづらい。そこで登場するのが、 Promise
ラッパーです。
/* plugins/Firebase.ts */
// ユーザーを取得する
// firebase.auth の currentUser を確実に取得するための Promise ラッパー
export function getCurrentUser(): Promise<any> {
return new Promise((resolve) => {
const unsubscribe = auth.onAuthStateChanged((user) => {
resolve(user);
unsubscribe();
});
});
}
👆のコードのように、Promise ラッパーを準備してやることで、呼び出しもとでは async/await
で呼び出すことができるようになります。また、何度も述べているように、 onAuthStateChanged
はオブザーバーなので、呼び出されるたびに購読を解除します(でないとオブザーバーが多重登録されていくことになってしまいます。)
認証が必要なページにリクエストが来た場合に、認証されていない状態の場合はログインページに飛ばす、などはよくあるユースケースだと思います。このように、ページがレンダリングされる前に判定処理を加えるのには Middleware が便利です。 📙 Nuxt 公式ドキュメント
ここは好みが分かれるところですが、僕は Nuxt の Layout 層に無名 Middleware を仕込むのが好みです。Layout 層に、認証が必要でないページと、認証が必要なページで分けてファイルを作り、それぞれの Page 層でどちらのレイアウトを使うかを選択する方式を取っています。
認証が必要なページの方が多いアプリケーションの場合は、 layout/default.vue
のデフォルトページの方を認証必須ページにしています(世の中の Web アプリケーションはだいたいこうなる気がする)。
/* layout/default.vue */
async middleware({ redirect }) {
console.log('=== middleware');
const currentUser = await getCurrentUser();
if (!currentUser) {
redirect('/signin');
}
}
という処理のフローにしています。
さいごに。API サーバへのリクエスト時にヘッダーに認証情報を詰める際にも、先述の Promise ラッパーを使用します。便利ですね。
async getIdToken(): Promise<string> {
console.log('=== getIdToken');
const currentUser = await getCurrentUser();
if (!currentUser) {
throw new Error('uninitialized currentUser');
}
const res = await currentUser.getIdToken().catch((e: any) => {
throw e;
});
return res;
}
以上の👆の流れを図にすると、以下のようになります。
同じアーキテクチャで web アプリケーションを構築する方は参考にしてみてください。
🦥(筆者が喜びます)