メモです

メモです

Next.jsでFirebase Authenticationをやる(next-firebase-auth)

追記:有名なNextAuht.jsでも同じことができるのでそっちのほうがいいかも、、、。NextAuthは多機能すぎるのでこっちでもいいが、、、
zenn.dev

next-firebase-authの概要

・サーバ側でFirebaseのIDトークンの情報(Twitter認証していたらアイコンとかスクリーンネームが入ってる)が使える
・クライアント側ではconst user = useAuthUser でFirebaseのIDトークンの情報が使える
・firebase v9もbeta版では対応中
・一応、Next.js公式からリンクがある
GitHub - gladly-team/next-firebase-auth: Simple Firebase authentication for all Next.js rendering strategies

認証の流れ

  1. Firebase Auth(react-firebaseuiとかで実装する)でTwitte等で認証する。これでFirebaseのIDトークンが生成される。
  2. headerのauthorizationに入ってるFirebaseのIDトークンを取得。FirebaseのIDトークンに入ってるスクリーンネームなどを情報をカスタムIDトークンに入れる
  3. カスタムIDトークンをhttp-onlyのクッキーに入れる(setAuthCookies.js)
  4. サーバ側ではクッキーからIDトークンを取得することで情報を利用。
  5. トークンの検証にはverifyIdTokenを使うことができる(これ用にfirebase-adminを使っている)。APIを保護する(認証済みユーザ以外には使わせない)際などに利用できる(exampleではapi/example.jsに例がある)
  6. logout時はクッキーを削除する

トークンをクッキーに入れる理由

IDトークン(JWT)をlocalStorageに入れることもあるが、localStorageに入れると

  1. XSSに対して脆弱になる

(XSSされる状況でわざわざIDトークンを盗むとかしないだろという反論はある)

  1. Google Analyticsなど第三者が作成したScriptを読み込むことが多いが、それらからも読み取ることが可能になってしまう

(Googleはそんなことしないとは思うが、じゃあどのScriptまでは信用できるのか、第三者が作成するScriptを追加するたびに問題ないかソースコードを全部チェックするのかという問題)
ということらしい。

手順

インストール

git clone https://github.com/gladly-team/next-firebase-auth
cd next-firebase-auth
yarn add next-firebase-auth
yarn add firebase firebase-admin next react react-dom

exampleディレクトリでサンプル用のprojectがあるのでそれをいじっていく

設定

next-firebase-auth/example/.env.local.exampleを開いて

  • NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY
  • NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
  • NEXT_PUBLIC_FIREBASE_PROJECT_ID
  • FIREBASE_PRIVATE_KEY

の4つをfirebaseのコンソール画面の中から探して埋める。
(左上の歯車クリックして、Firebase SDK snippetのところに書いてある)
他は埋めなくても動く。

※NEXT_PUBLIC_FIREBASE_DATABASE_URLはRealtimedatabaseを使う時に使うっぽい?

Firebase Authの設定画面でメールアドレスの認証をオンにする

実行

next-firebase-authのexampleディレクトリに戻り、

npm run dev

を実行する。

localhost:3000を開くと画面が立ち上がっているので、
Example: SSR + data fetching with ID token
とかをクリックして適当なメールアドレスを入力する。

Firebase Authのコンソール画面で見るとユーザができていると思う

なお、もう一度Example: SSR + data fetching with ID tokenを開くとYour favorite color is: が表示されるが、これはapi/example.jsを叩いてるだけなので毎回変わる
なお、api/example.jsではAPI保護のためのIDトークン検証を行っっている。

各ページの内容

static-auth-required-loader.js(Example: static + loader + data fetching with ID token)

クライアント側でデータを取得している。このページに遷移した際の挙動は以下の部分で指定している

whenUnauthedBeforeInit: AuthAction.SHOW_LOADER,
whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
LoaderComponent: FullPageLoader,
  • whenUnauthedBeforeInit : クライアントのJS SDKを初期化する前だった場合、一度ローディング画面を表示する設定。ローティング画面自体はLoaderComponentで指定している。
  • whenUnauthedAfterInit : 初期化が終わっているが、ログインされていない場合にはログインページに遷移する設定。

SNS認証

メール認証からTwitter認証に変える場合は
components/FirebaseAuth.jsにある
変更前:

provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,

変更後:

provider: firebase.auth.TwitterAuthProvider.PROVIDER_ID,

に変更する
他のSNSに変えたい場合はこちらのドキュメントを参照する
AuthProvider | JavaScript SDK  |  Firebase

ちなみにexample.jsはemail情報がないとサインインボタンを表示してしまうため、
各ページのHeaderコンポーネント
変更前:

<Header email={AuthUser.email} signOut={AuthUser.signOut} />

変更後:

<Header email={AuthUser.id} signOut={AuthUser.signOut} />

に変えるとちゃんとサインアウトボタンが出る。

トラブルシューティング

ページを開いている途中で
Module not found: Can't resolve 'react-firebaseui/StyledFirebaseAuth'
がでたら

npm install react-firebaseui --save --legacy-peer-deps

でなおる

設定

utilsディレクトリのinitAuth.js内で以下を設定できる

  • authPageURL : 未ログイン時のリダイレクト先
  • appPageURL : ログイン成功時のリダイレクト先
  • loginAPIpoint : ログイン用のendpoint

基本的な使い方

クライアント側でログインしているアカウント情報を取得する
import { useAuthUser, withAuthUser } from 'next-firebase-auth'

const Demo = () => {
  const AuthUser = useAuthUser()
  return (
    <div>
      <p>Your email is {AuthUser.email ? AuthUser.email : "unknown"}.</p>
    </div>
  )
}

export default withAuthUser({
  whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
})(Demo)
  • AuthUserに今ログインしているユーザの情報が入ってる
  • withAuthUserでルートとなるコンポーネントをラップする
  • {whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,}をつけると未ログイン時はログインページに飛ぶ。ホームページなどログイン状態が関係ないページの場合は消して良い。

Cloud Firestoreとの連携方法

クライアント側

クライアント側でfirestoreからフェッチするやり方はここに書いてある
https://github.com/gladly-team/next-firebase-auth#getfirebaseclient--firebaseapp

サーバ側

サーバサイドでfirestoreからフェッチするやり方。
ログインしたユーザのみに見せたいページの時とかに使う。
withAuthUserを使うことで未ログイン時にはログインページに遷移する。

import { 
  withAuthUser,
  AuthAction,
  withAuthUserTokenSSR,
  getFirebaseAdmin } from 'next-firebase-auth'

export const getServerSideProps = withAuthUserTokenSSR({
  whenUnauthed: AuthAction.REDIRECT_TO_LOGIN,
})(async (context) => {
  const db = getFirebaseAdmin().firestore()
  const doc = await db.collection('example').get()

// Firestoreから取得したデータを処理する
(中略)

// クエリ取得
  console.log('query', context.query)
// トークン検証済みのユーザ情報
 console.log('AuthUser', context.AuthUser)
  return { props: { uid: context.AuthUser.id } }
})

const MyLoader = () => <div>Loading...</div>

export default withAuthUser({
  whenUnauthedBeforeInit: AuthAction.SHOW_LOADER,
  whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
  LoaderComponent: MyLoader,
})(MainComponent)

ちなみに

・サーバ側でfirestoreからデータを取得
・ログインしていないユーザの場合、閲覧できないようにログイン画面へリダイレクト
のつもりで以下のようなコードを書くと
zenn.dev
にある脆弱性を引き起こすので注意。

※ getServerSideProps = withAuthUserTokenSSRのようにすると何もreturnせずにリダイレクトされる

import { getFirebaseAdmin } from 'next-firebase-auth'
// ...other imports

const Artist = ({artists}) => {
  return (
    <ul>
      {artists.map((artist) => <li>{artist.name}</li>)} 
    </ul>
  )
}

export async function getServerSideProps({ params: { id } }) {
  const db = getFirebaseAdmin().firestore()
  const doc = await db.collection('artists').get()
  return {
    props: {
      artists: artists.docs.map((a) => {
       return { ...a.data(), key: a.id }
      }),
    }
  }
}

export default withAuthUser({
  whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
})(Artist)