🧍

App Router で NextAuth.js 4.x を設定してみる

2024/02/25に公開

モチベーション

Next + NextAuth で認証機能をもつアプリを開発しようとドキュメントを参照したところ、Next公式のチュートリアル では App Router + next-auth 5.x (beta) という構成で書かれていました。

一方の NextAuth公式のドキュメント では Pages Router + next-auth 4.x で例示されていました。5はbetaなので仕方なしですが、つまり App Router + next-auth 4.x の実装方法が公式に示されていない。

beta はいずれ外されるとはいえ現時点ではプロダクションで使いたくはないので、それぞれのドキュメントを参考にしつつ素振りをしてみました。つまり、この記事は近日中に意味を成さないものになるでしょう。

インストール

Next.js と NextAuth をインストールします。

$ npx create-next-app@latest
$ npm i next-auth -S

もしこれで next-auth の v5 がインストールされたら、もうこの先を読む必要はありません。

設定

基本的に NextAuth 公式の Getting Started を参考に進めます。この記事では、ドキュメントと変えなければいけない手順を、最低限の内容だけ記載するので、事前に公式の手順に従って素振りをしておくとよいです。

cf) Getting Started | NextAuth.js

ENV

認証に使うIDなどの情報は .env にまとめておきます。わたしはよく AUTH_SECRET を忘れて怒られるので忘れないようにします。

/.env
GOOGLE_CLIENT_ID=XXXXXXXXXX
GOOGLE_CLIENT_SECRET=XXXXXXXXXX
AUTH_SECRET=XXXXXXXXXX

authOptions

NextAuthの設定は別ファイルに逃がしておきます。公式では pages/api/auth/[...nextauth].jsauthOptions を作って export していましたが、ここにあるとちょっと気持ちが悪いので別の場所に移動します。今回は /config/auth.ts に記述しました。

/config/auth.ts
import Google from 'next-auth/providers/google'

export const authOptions = {
  secret: process.env.AUTH_SECRET,
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID ?? '',
      clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? ''
    })
  ]
}

API Routes

Pages Router と最も大きく異なるのはここです。公式では /pages/api/auth/[...nextauth].js に記述がありますが、 App Router の場合はルーティングの構成が異なるので /app/api/auth/[...nextauth]/route.ts に実装します。

/app/api/auth/[...nextauth]/route.ts
import { authOptions } from '@/config/auth'
import NextAuth from 'next-auth/next'

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

Pages Router では NextAuth(authOptions) を直接 export default しましたが、App Router では GETPOST としてそれぞれを別々に export しなければいけないので注意が必要です。

LoginButton

ログインとログアウトを行うボタンコンポーネントを作成してページに配置します。
まずはログイン状態にかかわらず両方のボタンを表示させます。

/components/LogoutButton.tsx
'use client'

import { signIn, signOut } from 'next-auth/react'

export function LoginButton (): JSX.Element {
  const handleLogin = async (): Promise<void> => {
    await signIn()
  }
  const handleLogout = async (): Promise<void> => {
    await signOut()
  }

  return (
    <>
      <button onClick={handleLogin}>
        ログイン
      </button>
      <button onClick={handleLogout}>
        ログアウト
      </button>
    </>
  )
}
/app/dashboard/page.tsx
import { LoginButton } from '@/components/LoginButton'

export default function Page (): JSX.Element {
  return (
    <>
      <h1>Dashboard</h1>
      <LoginButton />
    </>
  )
}

SessionProvider

下のコードは公式の例ですが、クライアントサイドで useSession を使用するために SessionProvider でアプリ全体をラップしています。が、取り急ぎ useSession を使う想定はないのでこれはスキップします。

/pages/_app.tsx
import { SessionProvider } from "next-auth/react"
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

App Router で SessionProvider を使うためには "use client" を指定しなければいけませんが、ルートに近いところでクライアントコンポーネントにはしたくないのと、App Router なら Server Action で getServerSession() を使ってセッション情報を取得できるので、まずは必要ないかと考えています。(必要になったらその時また考えます)

ログイン状態の取得

ログイン状態の取得には Server Action を使ってみます。 getServerSession() を使ってセッション情報を読み、 emailgetUser() からユーザーを引いて返す、みたいな想定です。そのあたりの実装はまるっと割愛している(というかまだ考えてない)のでイメージしてください。

/app/actions.ts
'use server'

import { authOptions } from '@/config/auth'
import { getServerSession } from 'next-auth'

export async function getCurrentUser (): Promise<User | null> {
  try {
    const session = await getServerSession(authOptions)
    const user = await getUser({ email: session?.user?.email })
    return user
  } catch (error) {
    return null
  }
}

これをコンポーネント側で利用します。

/app/dashboard/page.tsx
import { LoginButton } from '@/components/LoginButton'
import { getCurrentUser } from '@/app/actions'

export default async function Page (): Promise<JSX.Element> {
  const user = await getCurrentUser()

  return (
    <>
      <h1>Dashboard</h1>
      {user && <p>ようこそ {user.name}</p>}
      <LoginButton isLoggedIn={Boolean(user)} />
    </>
  )
}

ついでに LoginButtonisLoggedIn prop を追加してログイン時と非ログイン時でログインボタンの表示を出し分けしてみましょう。ログイン時はログアウトボタンを、非ログイン時にはログインボタンを表示します。

/components/LoginButton.tsx
'use client'

import { signIn, signOut } from 'next-auth/react'

interface Props {
  isLoggedIn: boolean
}

export function LoginButton ({ isLoggedIn }: Props): JSX.Element {
  const handleLogin = async (): Promise<void> => {
    await signIn()
  }
  const handleLogout = async (): Promise<void> => {
    await signOut()
  }

  return isLoggedIn ? (
    <button onClick={handleLogout}>ログアウト</button>
  ) : (
    <button onClick={handleLogin}>ログイン</button>
  );
}

最低限の動作確認ができました。

感想

Server Action のおかげで、App Router の方がシンプルに実装できそうな予感がしています。

App Router はクライアントサイドとサーバーサイドの境界が明確にされていると見せかけて、結構自分が今どっちを書いているのか迷子になりがちな印象で、しかしながら「あ、これここに書いて動くんだ!」的な発見もあり、そのあたりが App Router のすごいところでもあり、キモいところでもあると思っています。

Discussion