🔐

NextAuthのセッションをサーバーサイドで管理してみる

2023/11/24に公開

はじめに

かなり久しぶりに記事を書きます。

自分は今とあるC向けのサービス開発に従事しており、そのサービスでは認証にNextAuthを使用しております。
今回の開発では、 JWTトークンをcookieで保持してセッション管理を行っており、Next.jsのRSCも使用していることからサーバーサイドでセッションの管理(ログイン・ログアウト)を管理したいという要件があったため、このやり方を紹介したいと思います。

結論

サーバーサイドでNextAuthのセッションをいじるには以下のようにしたら良さそうです。

  1. セッションアウトするには、cookieのjwtトークンを削除
  2. 新規セッションを作成するにはjwtトークンをencodeし、cookieセットする

やり方

NextAuthのセッティング

今回は、sessionで取得な可能な情報にDBで付与されているuuidやDBで管理されているname、さらにはrole情報などを含めたいため色々カスタマイズしております。

/lib/next-auth/options.ts
import { getUserWithUnAuthByEmail } from "@/service/server/graphql/query/getUserWithUnAuthByEmail";
import { SessionUserType } from "@/types/auth/session";
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import TwitterProvider from "next-auth/providers/twitter";

export const authOptions: NextAuthOptions = {
  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/signin",
  },
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
          provider: "google",
          role: "user",
        };
      },
    }),
    TwitterProvider({
      clientId: process.env.X_CLIENT_ID!,
      clientSecret: process.env.X_CLIENT_SECRET!,
      profile: (profile) => {
        return {
          id: profile.id,
          name: profile.name,
          email: profile.email,
          image: profile.profile_image_url,
          provider: "twitter",
          role: "user",
        };
      },
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "メールアドレス", type: "email" },
        password: { label: "パスワード", type: "password" },
      },
      async authorize(credentials, _req) {
        const email = credentials?.email;
        const password = credentials?.password || "";

        const user = {
          id: "",
          email,
          password,
          provider: "credentials",
          role: "user" as const,
        };

        return user ? user : null;
      },
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      const typedUser = user as SessionUserType | undefined;
      const password = typedUser?.password ?? undefined;

      if (typedUser?.email) {
        const serverCheck = await getUserWithUnAuthByEmail(typedUser.email);
        user.id = serverCheck?.id ?? "";
        user.role = "user";
        user.password = password;
      }

      return true;
    },
    jwt: async ({ token, user }) => {
      if (user) {
        const userRecord = await getUserWithUnAuthByEmail(user.email ?? "");

        user.role = "user";
        user.id = userRecord?.id ?? "";

        token.user = user;
      }

      return token;
    },
    session: async ({ session, trigger, token }) => {
      if (token) {
        session.user = token.user;
      }

      return session;
    },
  },
};

Route Handler側では、上記で作成したNextAuth option情報をもとに実装していきます。

/api/auth/[...nextauth]/route.ts
import { authOptions } from "@/lib/next-auth/options";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

セッションをいじる部分

Next.jsのcookies().delete(~)Route HandlerまたはServer Actionでしか実行できません。

https://nextjs.org/docs/app/api-reference/functions/cookies#deleting-cookies

今回はcookieを削除するだけのsign-outのRoute Handlerを実装してみました。
NextAuthのcookieのkey名はproduction / localで異なるので環境変数化してます。

/api/user/auth/sign-out/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  cookies().delete(process.env.NEXT_PUBLIC_SESSION_COOKIE_KEY_NAME ?? "");

  return NextResponse.redirect(`${process.env.NEXT_PUBLIC_APP_URL}/`);
}

使い方はかなりシンプルで、以下のようにfetchで叩くだけです。

await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/user/sign-out`);

続いて、セッション情報の更新・新規作成です。
今回はセッションの更新は「古いjwtの破棄→新規jwtをcookieにセット」というやり方で実装するため一緒くたに紹介します。

/lib/auth/update-session.ts
"use server";

import { getCookieSession } from "@/lib/auth/get-cookie-session";
import { encode } from "next-auth/jwt";
import { cookies } from "next/headers";

/**
 * NOTE:
 *  更新されうるプロパティを受け取る
 */
export const updateCookieSession = async (
  name?: string,
  image?: string
): Promise<void> => {
  const defaultSession = await getCookieSession();

  // 古いsession及びcookieの破棄
  await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/auth/user/sign-out`);

    // 新たなjwtトークンの生成
  const encodedJwtToken = await encode({
    token: {
      user: {
        id: defaultSession?.user?.id ?? "",
        email: defaultSession?.user?.email ?? "",
        name: name ?? defaultSession?.user?.name ?? "",
        image: image ?? defaultSession?.user?.image ?? "",
        provider: defaultSession?.user?.provider ?? "",
        role: defaultSession?.user?.role ?? "",
      },
    },
    secret: process.env.NEXTAUTH_SECRET ?? "",
  });

  cookies().set(
    process.env.NEXT_PUBLIC_SESSION_COOKIE_KEY_NAME ?? "",
    encodedJwtToken
  );
};

await encode({ ... })で新たなjwtトークンを生成後cookies().set()でcookieをセットします。cookies().delete()と同様にcookies().set()もRoute HanderまたはServer Actionでしか実行できません。先ほど、Route Handerを使用したため、今回はServer Actionで実装してみました。

また、今回はawait getCookieSession();という特殊なメソッドを使用しています。
これは自作のセッション情報を取得するメソッドなのですが、従来NextAuthにはgetServerSessionという備え付けのメソッドがあります。

しかし、今回の僕のようにsession情報をいじった際に、getServerSessionではデフォルトのnameemailimageの三つのプロパティしか取得してくれなかったため自作することにしました。

以下がgetCookieSessionの実装です。

/lib/auth/get-cookie-session.ts
"use server";

import { DefaultSessionUserType } from "@/types/auth/session";
import { decode } from "next-auth/jwt";
import { cookies } from "next/headers";

export const getCookieSession = async (): Promise<{
  user: DefaultSessionUserType;
} | null> => {
  const token = cookies().get(
    process.env.NEXT_PUBLIC_SESSION_COOKIE_KEY_NAME ?? ""
  )?.value;

  const session = (await decode({
    token,
    secret: process.env.NEXTAUTH_SECRET ?? "",
  })) as { user: DefaultSessionUserType } | null;

  return session;
};

やってることは至ってシンプルで、cookieからjwtトークンを取得してdecodeしたデータを返しているだけです。

最後に

cookies managedなセッション管理はセキュリティ的に賛否両論あるかと思います。
ましてや今回の自分にように直接いじるのは個人的には微妙な気がしています。

RSC時代のセッション管理の一つのやり方としてご参考までに

では良いNextAuthライフを🥳

Discussion