Route HandlersでNextAuth.jsのセッションが取れなくて困った
Next.js(13.4.5)を使って個人開発をしています。認証機能の実装にNextAuth.js(4.22.1)を利用したところ、困りがあったのでその内容と解決方法を共有します。もっといい解決方法などあればご指摘お願いします。
ことのおこり
まず、ドキュメントにある通り、もろもろの設定を行いました。
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,
};
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 };
"use client";
import { SessionProvider } from "next-auth/react";
import type { FC, PropsWithChildren } from "react";
export const NextAuthProvider: FC<PropsWithChildren> = ({ children }) => {
return <SessionProvider>{children}</SessionProvider>;
};
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
を使用し、ログインしているユーザーをセッションから取得してみます。
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;
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エラーを返すみたいなことをしてみたいです。
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 });
};
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>;
};
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
がどのようにしてセッションを取得しているのか見てみます。
引数の数が0もしくは1の場合はReact Server Componentでの利用であると判断し、const { headers, cookies } = require("next/headers")
でheaders
とcookies
を取得。headers
とcookies
をAuthHandler
に渡し、sessionを作っているようです。
getServerSession
でセッションを取得できているサーバーコンポーネントとできていないRoute Handlerとで、headers
とcookies
がそれぞれどのように設定されているのか見ていきます。
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>;
};
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-token
、next-auth.callback-url
、next-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
へのリクエストにもそのまま設定してみます。
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/hello
でgetServerSession
からセッションを取得できたことがわかりました。
おわり
すべてのheadersをまるごとRoute Handlerへのリクエストに乗せてしまっているので、必要最低限のものだけ乗せるようにしたほうがいいかもしれません。たとえばheaders: { Cookie: cookies().getAll().map(({ name, value }) => `${name}=${value}`).join(";") }
とかしてcookiesだけ設定するようにしてみても動きました。
Discussion