Next.jsとTypeScriptに入門したバックエンドエンジニアです。 以前、Nuxt.jsのアプリケーションでFirebase Authenticationによる認証を試したのですが、似たようなことをNext.jsでもやってみたので、纏めておこうと思います。 (Nuxt.jsとFirebase Authenticationを試した際にこちらの記事を書きました。)
次の要件を満たす簡単なサンプルプリケーションを作ってみました。
「Authenticated server-side rendering with Next.js and Firebase」 という記事と、この記事を書かれた方がGithubに公開されている colinhacks/next-firebase-ssr を、参考に、というよりは写経に近い形で、内容を噛み砕きながら、同様のサンプルアプリケーションを作ってみました。
shimar/nextjs-firebase-auth-example
今回勉強になったなと思う部分を残しておこうと思います。
ReactにContextという機能があることを知りました。
このContextを使うことで、親コンポーネントからその子孫に対して、一様に値を共有できるようです。 今回のアプリケーションでは、認証済みのユーザー情報をページ〜コンポーネント間で共有するために、次のようなContextを作ると良いようです。
import { createContext } from 'react';
import firebaseClient from '../lib/firebase-client';
const AuthContext = createContext<{ user: firebaseClient.User | null }>({
user: null
});
export default AuthContext;
さらに、
全てのコンテクストオブジェクトにはプロバイダ (Provider) コンポーネントが付属しており、これによりコンシューマコンポーネントはコンテクストの変更を購読できます。
とあるように、ContextにはProviderというコンポーネントがくっついていて、このコンポーネントの子孫のコンポーネントに、Contextが管理する情報が共有されるという仕組みのようです。
参考にさせて頂いたサンプルでは、上述のAuthContext
のプロバイダの中で、useEffect
を用いて、次のコトを行っています。
コードを見ていただくとわかると思いますが、今回の例では、FirebaseアカウントのIdTokenをクッキーに格納し、この値の有無で、認証済みか否かを判定しています。FirebaseアカウントのIdTokenの有効期限が1時間であることから、コレをリフレッシュして、クッキーに再設定する、という処理が必要になる、ということのようです。
これは、こんなコードになります。
import { useState, useEffect } from 'react';
import nookies from 'nookies';
import firebaseClient from '../lib/firebase-client';
import AuthContext from '../contexts/auth';
const AuthProvider = ({ children }: any) => {
const cookieKey = "token";
const interval = 10 * 60 * 1000;
const [user, setUser] = useState<firebaseClient.User | null>(null);
useEffect(() => {
return firebaseClient.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
nookies.destroy(null, cookieKey);
return;
}
const token = await user.getIdToken();
setUser(user);
nookies.destroy(null, cookieKey);
nookies.set(undefined, cookieKey, token, {});
});
}, []);
useEffect(() => {
const handler = setInterval(async () => {
const user = firebaseClient.auth().currentUser;
if (user) {
await user.getIdToken(true);
}
}, interval);
return () => clearInterval(handler);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
)
}
export default AuthProvider;
このProvider、つまり上記のコードのAuthProvider
は、基本的に全ページ共通の親となって良いと思われる(一般的に認証・認可が必要なアプリケーションでは認証が不要なページの方が少ないと考えています)ので、_app.tsx
で、次のように使っています。
import { AppProps } from 'next/app';
import AuthProvider from '../providers/auth';
import '../styles/globals.scss';
function MyApp({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
)
};
export default MyApp;
正直よくわかっていないのですが、Next.jsでSSR(Server Side Rendering)する際に、データの取得などをサーバーサイドで行う場合は、gerServerSideProps
関数を対象ページで実装し、その中でサーバーサイドの処理を行う、と認識しています。
今回は、サーバーサイドで認証状態を判定し、冒頭で書いた、
を実現しています。
リクエストされたパスと認証状態に応じて、リダイレクトする、或いはしない、という処理を行う関数を↓のように書いてみました。
import { GetServerSidePropsContext } from 'next';
import nookies from 'nookies';
import firebaseAdmin from '../../lib/firebase-admin';
const redirectTop = {
redirect: {
destination: '/',
permanent: false,
},
props: {} as never,
};
const redirectSignin = {
redirect: {
destination: '/signin',
permanent: false,
},
props: {} as never,
};
const empty = {
props: {},
};
const verifyAuthState = async (ctx: GetServerSidePropsContext) => {
const unauthenticated = ['/signin', '/signup'];
const cookies = nookies.get(ctx);
const url = ctx.req.url || '';
if (unauthenticated.includes(url)) {
if (cookies.token) {
return redirectTop;
} else {
return empty;
}
}
try {
await firebaseAdmin.auth().verifyIdToken(cookies.token);
return empty;
} catch (err) {
return redirectSignin;
}
};
export default verifyAuthState;
この関数を各ページのgetServerSideProps
関数で実行することで、認証の要否とそれに応じたリダイレクトが実現できるのでは?と考えました。
import verifyAuthState from '../utils/functions/verify-auth-state';
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
return verifyAuthState(ctx);
}
今回の実験では、ちょっと欲張って、tailwindcssを使ってみました。 (大した使い方はしていないです。)
Next.js + Firebase Authenticationを参考サイトと参考リポジトリを写経に近い形で試してみました。 この方法以外のアプローチがたくさんあると思いますので、ご意見あれば、GithubのIssueやTwitterなどで、ご指摘頂けると幸いです。
ちなみに次は、Firebase Authenticationについて得た諸々を記事にできればと思います。