7839

雑草魂エンジニアブログ

【Next】Next.jsの中で Firebase Admin SDK を使った処理をどのように実装すべきか

最近、Next.js に触れる機会があり、試行錯誤しながらコーディングを行っている。その中で、Next.js の中で Firebase Admin SDK をどのように実装すべきかという課題にぶち当たり、とりあえず実装はできたものの、最適解ではないと思っている。もし最善の策があれば教えて欲しい。切実に。

WEBアプリケーション構成

今回のWEBアプリケーションでは、Firebase Authentication でユーザー管理、ユーザー認証機能を実装している。簡単に書くと、以下のような構成になっている。

f:id:serip39:20200822185854j:plain

フロントエンドの実装で、Next.jsを用いている。SSRで実装しており、Custom Server として、Express を用いている。

Firebase Authentication

firebase.google.com

Firebase Authentication を使うことで、アプリケーションに簡単に認証機能を追加することができる。パスワード認証や OAuth2 に対応した主要なフェデレーション ID を使った認証などが用意されている。フェデレーション認証で、GoogleFacebookGitHubTwitter などのサードパーティの認証情報を使用して、アプリケーションにログインさせることも簡単に実装できる。簡単に実装できると書いたのは、使いやすい SDK、アプリでのユーザー認証に使用できる UI ライブラリまでも用意されているからである。簡単に、確実に導入ができるというメリットは本当に大きい。さすがGoogle様だ。

今回の Node.js の WEBアプリケーションの場合、以下のSDKを用いることができる。(SDKには、Authenticationの認証機能だけでなく、Firebaseの様々な機能を用いることができる。今回は、認証機能に絞って記載する。)

  • Firebase JavaScript SDK(フロントエンド用)

    • firebase.auth() で Authentication のメソッドを使うことができる
    • 新規ユーザー登録、ログイン、ログアウト、ユーザーデータの取得・更新が可能(ただし、ユーザー管理などは、サーバーからのみしかできない。)
  • Firebase Admin SDK(サーバーサイド用)

導入方法などは、公式を参照して欲しい。

firebase.google.com

firebase.google.com

Firebase Admin SDK を用いた ID トークン検証

Firebase JavaScript SDK を用いることで、基本的にはフロントエンドで実装したい機能は対実現できる。ただ、ログイン後に、IDトークンを Cookie などに保持し、リロードされた場合にもこの Cookie で保持した IDトークン を使用して、再度認証する場合を考える。

その場合には、サーバーサイドから確認をする必要がある。

firebase.google.com

そのため、今回の場合、Express の Custom Server から Firebase Admin SDK を用いて検証を行い、フロントエンドにユーザー情報を返却してあげる必要がある。

Next.js 内での実装方法

Next.js でどのように実装すべきかを検討するにあたり、色々な実装方法があった。

Next.js の API Routes を利用する

公式の example に実装例が用意されている。

next.js/examples/with-firebase-authentication at canary · vercel/next.js · GitHub

API Routes はサーバーサイド側で実行されるので、Firebase Admin SDK を用いることができる。

getServerSideProps と HOC を利用して実装する

github.com

直接的に、Firebase Authentication を実装されていないが、上記と同じようにすることで実装が可能そうであった。

Express の Custom Server に実装する

今回、私はこちらで実装を行ったので、実装例として紹介する。

Server側に関しては、Express に IDトークンを検証する機能を追加するだけであるが、問題はどのようにして、フロント側に認証情報を返却してあげるべきかということであった。

処理の経路は以下のようになる。

  1. ユーザーがブラウザからアクセスする。
  2. サーバー側にリクエストが来て、Expressで受け取る。
  3. Cookieを確認して、IDトークンを Firebase に確認して検証する。カスタムクレームを使用して、ユーザー権限に応じたページをレンダリングする。
  4. getInitialProps(getServerSideProps)の処理が実行される。
  5. propsでフロントにデータを受け渡す

IDトークンで認証した際に得られたユーザー情報をフロントに渡さずに、ユーザー情報をGETできる API を用意することもできるが、今回はAPIを使わずに、フロントにユーザー情報(カスタムクレーム)を返却する方法を考えた。

カスタムサーバー側では、以下の処理でレンダリングを行うことができる。

app.render(req, res, pathname, query, parsedUrl)

すなわち、レンダリングする前に、リクエストやクエリに情報を付加することができるのである。headerに情報を付加しようとして、 setHeader などを試したがうまくいかず、リクエストのクエリにデータを付加することにした。

app
  .prepare()
  .then(() => {
    const server = express()
    server.use(cookieParser())
    server.all('*', async (req: Request, res: Response) => {
      authenticateAccess(req)
        .then((newReq) => {
          if (newReq) {
            return app.render(newReq, res, req.url)
          }
          return handle(req, res)
        })
        .catch((err) => {
          res.writeHead(301, { Location: '/signin' })
          res.end()
        })
    })

    server.listen(port, (err?: any) => {
      if (err) throw err
      console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`)
    })
  })
  .catch((ex) => {
    console.error(ex.stack)
    process.exit(1)
  })

そして、ページ側では以下のようにして、getInitialPropsのreqのクエリデータから、ユーザー情報を受け取ることができた。

const AppComponent = ({ Component, pageProps }: AppProps) => {
  const { claims } = pageProps
  return <Component {...pageProps} />
}

AppComponent.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext)
  const { ctx } = appContext
  const isServer = !!ctx.req
  if (isServer) {
    const { claims } = ctx.req.query
    appProps.pageProps.claims = claims
  }
  return { ...appProps }
}

export default AppComponent

かなり無理矢理で実装することができた。。。

まとめ

今回は、Next.js + Firebase Authentication に関して、色々と試行錯誤した結果の実装例を紹介した。是非、もっとこうしたらいいのではないか等、ご意見やアドバイスがあれば、是非教えてほしい。

それでは、ステキな開発ライフを。