TL;DR

  • NextAuth.js v4 を Next.js v14 に導入する手順を、環境変数、Middleware、NextAuthOptions、Route Handler、getServerSession の順にまとめた。
  • 公式ドキュメントでは NEXTAUTH_SECRET は本番環境でのみ必須とされているが、getServerSession を使う場合は開発環境でも設定しないとエラーになる。

環境

  • NextAuth.js v4
  • Next.js v14

検証

1. 環境変数 NEXTAUTH_SECRET を設定する

公式ドキュメントには、secret について次のように書かれている。

  • Default value: string (SHA hash of the “options” object) in development, no default in production.
  • Required: Yes, in production!

しかし、getServerSession を使っていると、開発環境でも次のエラーが出た。

{
  message: 'decryption operation failed',
  stack: 'JWEDecryptionFailed: decryption operation failed\n' +
    '    at gcmDecrypt (webpack-internal:... +
    '    at decrypt (webpack-internal:...\n' +
    '    at flattenedDecrypt (webpack-internal:...\n' +
    '    at async compactDecrypt (webpack-internal:\n' +
    '    at async jwtDecrypt (webpack-internal:...\n' +
    '    at async Object.decode (webpack-internal:...\n'
    '    at async Object.session (webpack-internal:...\n' +
    '    at async AuthHandler (webpack-internal:...\n' +
    '    at async getServerSession (webpack-internal:...',
  name: 'JWEDecryptionFailed'
}

getServerSession の中では JWT のセッションをデコードしており、そのときに secret が使われるようである。公式ドキュメントにも次の記載がある。

A random string is used to hash tokens, sign/encrypt cookies and generate cryptographic keys.

値は openssl コマンドで生成した文字列を使うとよい。

$ openssl rand -base64 32

これを NEXTAUTH_SECRET=<生成した文字列> のように設定する。

2. Middleware を置く

Middleware を使うと、特定の URL パターンにマッチするルートを、認証が必要なルートとして扱える。次のコードでは、未認証の状態で /profile にアクセスするとログイン画面にリダイレクトされる。

// src/middleware.ts
export { default } from "next-auth/middleware"
 
export const config = { matcher: ["/profile"] }

3. NextAuthOptions を設定する

NextAuthOptions は、NextAuth.js を初期化するときに渡す設定値の型である。

import NextAuth from "next-auth"
 
const authOptions: NextAuthOptions = {
  // ...
}
const handler = NextAuth(authOptions)

adapter には、ユーザやアカウントのデータを永続化するアダプタを指定する。次は Prisma Client を使う例である。

import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/lib/prisma" // Prisma Client
 
export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  // ...
}

providers には、利用する OAuth のサービスを複数指定できる。次は Google の例である。

import GoogleProvider from "next-auth/providers/google"
 
export const authOptions: NextAuthOptions = {
  // ...
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID || "",
      clientSecret: process.env.GOOGLE_SECRET || "",
    }),
  ],
  // ...
}

callbacks では、次の 4 つのタイミングで呼ばれるコールバック関数を定義できる。

  • signIn: ユーザがサインインしようとしたときに呼ばれる。主にメールアドレス認証で役に立つ。true を返すとサインインを許可し、false を返すと許可せずにデフォルトのエラーメッセージを表示する。文字列を返すと、その URL にリダイレクトする。
  • redirect: サインインやサインアウトのときなど、コールバック URL にリダイレクトされるたびに呼ばれる。
  • jwt: JWT の作成時(サインイン)や更新時(クライアントでセッションにアクセスしたとき)に呼ばれる。session.strategy が jwt のときだけ有効で、戻り値は暗号化されて Cookie に保存される。
  • session: セッションがチェックされるたびに呼ばれる。具体的には、クライアントサイドの useSession、サーバサイドの getServerSession、/api/auth/session へのアクセスで呼ばれる。jwt の後に呼ばれるので、jwt で保存したデータを受け取れる。
export const authOptions: NextAuthOptions = {
  // ...
  callbacks: {
    async jwt({ token, user }) {
      const dbUser = await prisma.user.findFirst({
        where: { email: token.email },
      })
      if (!dbUser) {
        if (user) {
          token.id = user?.id
        }
        return token
      }
      return {
        id: dbUser.id,
        name: dbUser.name,
        email: dbUser.email,
        picture: dbUser.image,
      }
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.name = token.name
        session.user.email = token.email
        session.user.image = token.picture
      }
      return session
    },
  },
}

4. Route Handler を置く

NextAuth.js は専用の Route Handler、api/auth/[...nextauth]/route.ts を使う(ドキュメント)。ここでは NextAuth の戻り値を GET と POST として export する。認可サーバからのリダイレクト URL も、この Route Handler で処理する。

// api/auth/[...nextauth]/route.ts
const handler = NextAuth(authOptions)
 
export { handler as GET, handler as POST }

5. getServerSession を使う

getServerSession は、サーバサイドでログイン情報を参照するための関数で、NextAuthOptions を引数に受け取る。クライアントサイドでは useSession を使う。

export default async function Page() {
  const session = await getServerSession(authOptions)
  const profile = await prisma.profile.findUnique({
    where: { userId: session?.user.id },
  })
 
  return /* ... */
}

毎回 authOptions を渡すのは手間なので、あらかじめ適用した関数を用意しておくとよい。ついでに React.cache も適用しておく。

import { getServerSession as originalGetServerSession } from "next-auth"
 
export const getServerSession = cache(async () => {
  return originalGetServerSession(authOptions)
})

キーワード

参考