🍪

Route HandlersでNextAuth.jsのセッションが取れなくて困った

2023/06/20に公開

Next.js(13.4.5)を使って個人開発をしています。認証機能の実装にNextAuth.js(4.22.1)を利用したところ、困りがあったのでその内容と解決方法を共有します。もっといい解決方法などあればご指摘お願いします。

ことのおこり

まず、ドキュメントにある通り、もろもろの設定を行いました。

src/libs/next-auth/options.ts
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import GoogleProvider from "next-auth/providers/google";

import client from "@/libs/prisma/client";

import type { NextAuthOptions } from "next-auth";

export const nextAuthOptions: NextAuthOptions = {
  debug: true,
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  adapter: PrismaAdapter(client),
  callbacks: {
    session: ({ session, user }) => {
      return {
        ...session,
        user: {
          ...session.user,
          id: user.id,
        },
      };
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
};

src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";

import { nextAuthOptions } from "@/libs/next-auth/options";

const handler = NextAuth(nextAuthOptions);

// https://next-auth.js.org/configuration/initialization#route-handlers-app
export { handler as GET, handler as POST };

src/libs/next-auth/provider.tsx
"use client";

import { SessionProvider } from "next-auth/react";

import type { FC, PropsWithChildren } from "react";

export const NextAuthProvider: FC<PropsWithChildren> = ({ children }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

src/app/layout.tsx
import { NextAuthProvider } from "@/libs/next-auth/provider";

import type { FC, PropsWithChildren } from "react";

const RootLayout: FC<PropsWithChildren> = ({ children }) => {
  return (
    <html lang="ja">
      <body>
        <NextAuthProvider>{children}</NextAuthProvider>
      </body>
    </html>
  );
};
export default RootLayout;

prisma-adapterの設定なども行いましたが、記事の主題とは関係ないため省略します。

getServerSessionを使用し、ログインしているユーザーをセッションから取得してみます。

src/app/page.tsx
import { Suspense } from "react";

import { Container, Heading, Spinner, Stack } from "@/libs/chakra-ui";

import { LogoutButton } from "@/features/auth/logout-button";
import { Name } from "@/features/user/name";

import type { NextPage } from "next";

const Page: NextPage = () => {
  return (
    <Container>
      <Stack direction="column">
        <Heading>ようこそ</Heading>
        <Suspense fallback={<Spinner />}>
          <Name />
        </Suspense>
        <LogoutButton />
      </Stack>
    </Container>
  );
};
export default Page;

src/features/user/name.tsx
import { getServerSession } from "next-auth";

import { Text } from "@/libs/chakra-ui";
import { nextAuthOptions } from "@/libs/next-auth/options";

import type { FC } from "react";

export const Name: FC = async () => {
  const session = await getServerSession(nextAuthOptions);

  return <Text>{session?.user.name ?? "-"}</Text>;
};

セッションの取得に成功し、ユーザーの名前を表示することができました。

セッションの取得に失敗したらログインページにリダイレクトする、みたいな処理が書けそうです。

Route Handlersでも同様にセッションを取得し、認証されていない場合は401エラーを返すみたいなことをしてみたいです。

src/app/api/hello/route.ts
import { getServerSession } from "next-auth/next";
import { NextResponse } from "next/server";

import { nextAuthOptions } from "@/libs/next-auth/options";

export const GET = async (req: Request) => {
  const session = await getServerSession(nextAuthOptions);

  if (!session) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ message: "Hello" }, { status: 200 });
};

src/features/hello/greet.tsx
import { Text } from "@/libs/chakra-ui";

import type { FC } from "react";

export const Greet: FC = async () => {
  const response = await fetch("http://localhost:3000/api/hello", {
    cache: "no-cache",
  });
  const hello = await response.json();

  return <Text>{JSON.stringify(hello)}</Text>;
};

src/app/page.tsx
import { Suspense } from "react";

import { Container, Heading, Spinner, Stack } from "@/libs/chakra-ui";

import { LogoutButton } from "@/features/auth/logout-button";
import { Greet } from "@/features/hello/greet";
import { Name } from "@/features/user/name";

import type { NextPage } from "next";

const Page: NextPage = async () => {
  return (
    <Container>
      <Stack direction="column">
        <Heading>ようこそ</Heading>
        <Suspense fallback={<Spinner />}>
          <Name />
        </Suspense>
        <Suspense fallback={<Spinner />}>
          <Greet />
        </Suspense>
        <LogoutButton />
      </Stack>
    </Container>
  );
};
export default Page;

getServerSessionでセッションが取得できれば、<Greet />{"message":"Hello"}を返すはずです。

実行してみると、<Greet />{"message":"Hello"}ではなく{"message":"Unauthorized"}を表示しました。Route Handler内のgetServerSession(nextAuthOptions)nullを返しているためです。

なぜセッションが取れなかったのか

getServerSessionがどのようにしてセッションを取得しているのか見てみます。

https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/next/index.ts#L166-L234

引数の数が0もしくは1の場合はReact Server Componentでの利用であると判断し、const { headers, cookies } = require("next/headers")headerscookiesを取得。headerscookiesAuthHandlerに渡し、sessionを作っているようです。

getServerSessionでセッションを取得できているサーバーコンポーネントとできていないRoute Handlerとで、headerscookiesがそれぞれどのように設定されているのか見ていきます。

src/features/hello/greet.tsx
import { Text } from "@/libs/chakra-ui";

import type { FC } from "react";

export const Greet: FC = async () => {
  console.log("server component");
  console.log({
    headers: Object.fromEntries(headers()),
    cookies: cookies().getAll(),
  });

  const response = await fetch("http://localhost:3000/api/hello", {
    cache: "no-cache",
  });
  const hello = await response.json();

  return <Text>{JSON.stringify(hello)}</Text>;
};

src/app/api/hello/route.ts
import { getServerSession } from "next-auth/next";
import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";

import { nextAuthOptions } from "@/libs/next-auth/options";

export const GET = async (req: Request) => {
  console.log("route handler");
  console.log({
    headers: Object.fromEntries(headers()),
    cookies: cookies().getAll(),
  });
  const session = await getServerSession(nextAuthOptions);

  if (!session) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ message: "Hello" }, { status: 200 });
};

コンソールで確認してみると、cookies(next-auth.csrf-tokennext-auth.callback-urlnext-auth.session-token)などのheadersがサーバーコンポーネントの方には設定されていましたが、Route Handlerの方には設定されていませんでした。

Route Handlerでは、必要なheadersが設定されていなかったからgetServerSessionがセッションを取得できなかったと考えてみます。

どう解決したか

(表現が正しいかどうかは自信ありませんが)次のような状態であると考えました。

  • クライアントからlocalhost:3000へのリクエストには必要なheadersが設定されている
  • <Greet />からlocalhost:3000/api/helloへのリクエストには必要なheadersが設定されていない

そこで、クライアントからlocalhost:3000へのリクエストに設定されているheadersを、<Greet />からlocalhost:3000/api/helloへのリクエストにもそのまま設定してみます。

src/features/hello/greet.tsx
import { headers } from "next/headers";

import { Text } from "@/libs/chakra-ui";

import type { FC } from "react";

export const Greet: FC = async () => {
  const response = await fetch("http://localhost:3000/api/hello", {
    cache: "no-cache",
    headers: Object.fromEntries(headers()),
  });
  const hello = await response.json();

  return <Text>{JSON.stringify(hello)}</Text>;
};

<Greet />{"message":"Hello"}を表示し、/api/hellogetServerSessionからセッションを取得できたことがわかりました。

おわり

すべてのheadersをまるごとRoute Handlerへのリクエストに乗せてしまっているので、必要最低限のものだけ乗せるようにしたほうがいいかもしれません。たとえばheaders: { Cookie: cookies().getAll().map(({ name, value }) => `${name}=${value}`).join(";") }とかしてcookiesだけ設定するようにしてみても動きました。

GitHubで編集を提案
アガルートテクノロジーズ/PrAha

Discussion