🤖

Next.js で Auth.js v5 Beta を利用する

2024/06/20に公開

はじめに

この記事では Next.js App Router で Auth.js の V5 Beta を利用します。

具体的には、以下の動画で紹介されている内容を少し修正しながら実装していきます。

https://www.youtube.com/watch?v=z2A9P1Zg1WM

Auth.jsとは❓

Auth.js とは認証機能を実装できるライブラリです。以前は NextAuth とも呼ばれていました。

https://authjs.dev/

Next.js のプロジェクトを作成

作業用に Next.js プロジェクトを作成します。長いので、折り畳んでおきます。

作業用の新規に Next.js プロジェクトを作成します。

プロジェクトの作成

create next-app@latestでプロジェクトを作成します。

$ pnpm create next-app@latest next-auth-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-auth-sample 

不要な設定を削除し、プロジェクトを初期化します。

stylesの初期化

CSSなどを管理するstylesディレクトリを作成します。globals.cssを移動します。

$ mkdir -p src/styles
$ mv src/app/globals.css src/styles/globals.css

globals.cssの内容を以下のように上書きします。

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

初期ページの初期化

app/page.tsxを上書きします。

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

const Home: FC = () => {
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

レイアウトの初期化

app/layout.tsxを上書きします。

src/app/layout.tsx
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
  children: React.ReactNode;
};

export const metadata = {
  title: "Sample",
  description: "Generated by create next app",
};

const RootLayout: FC<RootLayoutProps> = (props) => {
  return (
    <html lang="ja">
      <body className="">{props.children}</body>
    </html>
  );
};

export default RootLayout;

TailwindCSSの設定

TailwindCSSの設定を上書きします。

tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  plugins: [],
}
export default config

TypeScriptの設定

TypeScriptの設定を上書きします。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

スクリプトを追加

型チェックのスクリプトを追加します。

package.json
{
  "scripts": {
+   "typecheck": "tsc"
  },
}

動作確認

型チェックします。

$ pnpm run typecheck

ローカルで動作確認します。

$ pnpm run dev

コミットして作業結果を保存しておきます。

$ git add .
$ git commit -m "作業用のプロジェクトを作成"

Auth.js を設定

Auth.js をインストール

Auth.js をインストールします。

$ pnpm install next-auth@beta

コミットします。

$ git add .
$ git commit -m "Auth.js をインストール"

Auth.js の環境を構築します。

AUTH_SECRET

Auth.js を利用するためには AUTH_SECRET が必要です。

2 つの方法で設定できます。

1 つ目が、npx auth secret を実行して、コンソールに表示された文字列を .env に追加します。

$ npx auth secret

Secret generated. Copy it to your .env/.env.local file (depending on your framework):

AUTH_SECRET=/Q3OtxWOSfKJTTJmeCGzD2DFtdl+OU0xEGX22hxlg9c=

2 つ目が、openssl rand -base64 32 を実行して、コンソールに表示された文字列を .env に追加します。

$ openssl rand -base64 32 | pbcopy

.envAUTH_SECRET を追加します。

$ touch .env
.env
AUTH_SECRET=secret

.gitignore に .env を追加します。

.gitignore
+.env

設定ファイルを作成

Auth.js の設定ファイルを作成します。このファイルで Auth.js がどの様に動作するかを設定します。

$ mkdir -p src/auth
$ touch src/auth/index.ts
src/auth/index.ts
import NextAuth, { User, NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";

// 認証APIのベースパス
export const BASE_PATH = "/api/auth";

const authOptions: NextAuthConfig = {
  providers: [
    Credentials({
      name: "Credentials",
      // 認証フォームのフィールド
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password" },
      },
      // 認証処理
      async authorize(credentials): Promise<User | null> {
        // ユーザー情報のダミーデータ
        const users = [
          {
            id: "test-user-1",
            userName: "test1",
            name: "Test 1",
            password: "qk5lSJ3maQ0pqmOyadTQRgN1K",
            email: "test1@example.com",
          },
          {
            id: "test-user-2",
            userName: "test2",
            name: "Test 2",
            password: "T2GapYCYK6wp8mJ1YUUnYpBMc",
            email: "test2@example.com",
          },
        ];
        // ユーザー情報の検索
        const user = users.find(
          (user) =>
            user.userName === credentials.username &&
            user.password === credentials.password
        );
        // ユーザー情報の返却
        return user
          ? { id: user.id, name: user.name, email: user.email }
          : null;
      },
    }),
  ],
  // 認証APIのベースパス
  basePath: BASE_PATH,
  // シークレットキーの設定
  secret: process.env.NEXTAUTH_SECRET,
};

export const { handlers, auth, signIn, signOut } = NextAuth(authOptions);

APIを作成

認証用のエンドポイントを作成します。

$ mkdir -p src/app/api/auth/\[...nextauth\]
$ touch src/app/api/auth/\[...nextauth\]/route.ts
src/app/api/auth/\[...nextauth\]/route.ts
import { handlers } from "@/auth";

export const { GET, POST } = handlers;

ページを修正

page.tsx を修正します。

src/app/page.tsx
+import { auth } from "@/auth";
import { type FC } from "react";

-const Home: FC = () => {
+const Home: FC = async () => {
+ const session = await auth();
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
+     <pre className="bg-slate-100 p-2 text-sm text-slate-700">
+       {JSON.stringify(session, null, 2)}
+     </pre>
    </div>
  );
};

export default Home;

動作確認

ローカルで動作確認します。

$ pnpm run dev

サインインするには、以下のリンクからサインインページに移動します。

http://localhost:3001/api/auth/signin

alt text

サインインするとセッション情報が表示されます。

alt text

サインアウトするには、以下のリンクからサインアウトページに移動します。

http://localhost:3001/api/auth/signout

alt text

サインアウトするとセッション情報が表示されません。

alt text

コミットします。

$ git add .
$ git commit -m "Auth.js をインストール、設定ファイルを作成、認証APIを作成、ページを修正"

サインイン、サインアウトボタンを追加

ヘルパー関数を作成し、signinsignout をエクスポートします。

$ touch src/auth/helpers.ts
src/auth/helpers.ts
"use server";
import { signIn as nextAuthSignIn, signOut as nextAuthSignOut } from ".";

export async function signIn() {
  await nextAuthSignIn();
}

export async function signOut() {
  await nextAuthSignOut();
}

クライアントとサーバで利用するボタンを明確に分けて作成します。

$ mkdir -p src/components/
$ touch src/components/AuthButton.client.tsx
$ touch src/components/AuthButton.server.tsx
src/components/AuthButton.server.tsx
import { SessionProvider } from "next-auth/react";
import { BASE_PATH, auth } from "@/auth";

import AuthButtonClient from "./AuthButton.client";

export default async function AuthButton() {
  const session = await auth();
  if (session && session.user) {
    session.user = {
      name: session.user.name,
      email: session.user.email,
    };
  }

  return (
    <SessionProvider basePath={BASE_PATH} session={session}>
      <AuthButtonClient />
    </SessionProvider>
  );
}
src/components/AuthButton.client.tsx
"use client";
import { useSession } from "next-auth/react";
import { signIn, signOut } from "@/auth/helpers";

export default function AuthButton() {
  const session = useSession();

  return session?.data?.user ? (
    <button
      className="bg-red-500 text-white px-4 py-2 rounded"
      onClick={async () => {
        await signOut();
        await signIn();
      }}
    >
      {session.data?.user?.name} : Sign Out
    </button>
  ) : (
    <button
      className="bg-blue-500 text-white px-4 py-2 rounded"
      onClick={async () => await signIn()}
    >
      Sign In
    </button>
  );
}

ページに反映します。

src/app/page.tsx
import { auth } from "@/auth";
+import AuthButton from "@/components/AuthButton.server";
import { type FC } from "react";

const Home: FC = async () => {
  const session = await auth();
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
      <pre className="bg-slate-100 p-2 text-sm text-slate-700">
        {JSON.stringify(session, null, 2)}
      </pre>
+     <AuthButton />
    </div>
  );
};

export default Home;

動作確認します。

$ pnpm run dev

ホームにアクセスすると、サインインボタンを表示されます。サインインボタンをクリックすると、サインインページに移動します。

http://localhost:3000/

alt text

ユーザー名とパスワードを入力してサインインします。

alt text

サインインすると、サインアウトボタンが表示されます。

alt text

コミットします。

$ git add .
$ git commit -m "サインイン、サインアウトボタンを追加"

保護ページを作成

サインインしていないとアクセスできないページを作成します。

保護ページを作成

まずページを作成します。

$ mkdir -p src/app/protected
$ touch src/app/protected/page.tsx
src/app/protected/page.tsx
import { auth } from "@/auth";

export default async function TestRoute() {
  const session = await auth();

  return (
    <main>
      <h1 className="text-3xl mb-5">Test Route</h1>
      <div>User: {session?.user?.name}</div>
    </main>
  );
}

middlewareを作成

続いて、middleware を作成します。middleware では、ユーザーが認証済みか判断し、認証済みでなければサインインページにリダイレクトします。

$ touch src/middleware.ts
src/middleware.ts
import { NextResponse } from "next/server";
import { auth, BASE_PATH } from "@/auth";

// api, _next/static, _next/image, favicon.ico以外のアクセスであればmiddlewareを通すという意味です。
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

export default auth((req) => {
  // ユーザーが認証済みか判断し、認証済みでなければサインインページにリダイレクトします。
  const reqUrl = new URL(req.url);
  if (!req.auth && reqUrl?.pathname !== "/") {
    return NextResponse.redirect(
      new URL(
        `${BASE_PATH}/signin?callbackUrl=${encodeURIComponent(
          reqUrl?.pathname
        )}`,
        req.url
      )
    );
  }
});

動作確認

動作確認します。

$ pnpm run dev

ログインしていない場合、保護ページにアクセスすると、サインインページにリダイレクトされます。

http://localhost:3000/protected

ログインしている場合は、保護ページにアクセスできます。

alt text

コミットします。

$ git add .
$ git commit -m "保護ページを作成"

Server Actions の追加

Server Actions を利用して認証情報を取得する方法を追加します。

Server Action を作成

Server Action を実装します。シンプルにユーザー名を返すだけの関数を作成します。

$ mkdir -p src/app/actions
$ touch src/app/actions/index.ts
src/app/actions/index.ts
"use server";
import { auth } from "@/auth";

export async function onGetUserAction() {
  const session = await auth();
  return session?.user?.name ?? null;
}

Server Action を利用するコンポーネントを作成

続いて、Server Action を利用するコンポーネントを作成します。

$ touch src/app/protected/WhoAmIServerAction.tsx
src/app/protected/WhoAmIServerAction.tsx
"use client";
import { useEffect, useState } from "react";
import { onGetUserAction } from "../actions";

export default function WhoAmIServerAction() {
  const [user, setUser] = useState<string | null>();

  useEffect(() => {
    onGetUserAction().then((user) => setUser(user));
  }, []);

  return <div className="mt-5">Who Am I (server action): {user}</div>;
}

Server Action をページに追加

ページを修正します。

src/app/protected/page.tsx
import { auth } from "@/auth";
+import WhoAmIServerAction from "./WhoAmIServerAction";

export default async function TestRoute() {
  const session = await auth();

  return (
    <main>
      <h1 className="text-3xl mb-5">Test Route</h1>
      <div>User: {session?.user?.name}</div>
+     <WhoAmIServerAction />
    </main>
  );
}

動作確認

動作確認します。

$ pnpm run dev

ログインして保護ページにアクセスすると、Server Action で取得したユーザー名が表示されます。

http://localhost:3000/protected

alt text

コミットします。

$ git add .
$ git commit -m "Server Actions の追加"

クライアントコンポーネントから認証情報を取得

ここでは、クライアントコンポーネントから API を通して認証情報を取得する方法を追加します。

API を作成

API を作成します。API はサインインしているユーザーの名前を返します。

$ mkdir -p src/app/api/whoami
$ touch src/app/api/whoami/route.ts
src/app/api/whoami/route.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export const GET = auth(async ({ auth }) => {
  return NextResponse.json({ user: auth?.user?.name });
});

APIを利用するコンポーネントを作成

API を実行し取得したユーザー名を表示するコンポーネントを作成します。

$ touch src/app/protected/WhoAmIAPI.tsx
src/app/protected/WhoAmIAPI.tsx
"use client";
import { useEffect, useState } from "react";

export default function WhoAmIAPI() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch("/api/whoami")
      .then((res) => res.json())
      .then(({ user }) => setUser(user));
  }, []);
  return <div className="mt-5">Who Am I (client): {user}</div>;
}

ページを修正

ページを修正します。

src/app/protected/page.tsx
import { auth } from "@/auth";
import WhoAmIServerAction from "./WhoAmIServerAction";
+import WhoAmIAPI from "./WhoAmIAPI";

export default async function TestRoute() {
  const session = await auth();

  return (
    <main>
      <h1 className="text-3xl mb-5">Test Route</h1>
      <div>User: {session?.user?.name}</div>
      <WhoAmIServerAction />
+     <WhoAmIAPI />
    </main>
  );
}

動作確認

動作確認します。

$ pnpm run dev

ログインして保護ページにアクセスすると、作成したコンポーネントで取得したユーザー名が表示されます。

http://localhost:3000/protected

alt text

コミットします。

$ git add .
$ git commit -m "API を通して認証情報を取得"

RSCから認証情報を取得

ここでは、RSC から API を通して認証情報を取得する方法を追加します。

RSCを作成

RSC を作成し、API を通して認証情報を取得します。

$ touch src/app/protected/WhoAmIRSC.tsx
src/app/protected/WhoAmIRSC.tsx
import { headers } from "next/headers";

export default async function WhoAmIRSC() {
  const { user } = await fetch("http://localhost:3000/api/whoami", {
    method: "GET",
    headers: headers(),
  }).then((res) => res.json());

  return <div className="mt-5">Who Am I (RSC): {user}</div>;
}

ページを修正

ページを修正します。

src/app/protected/page.tsx
import { auth } from "@/auth";
import WhoAmIServerAction from "./WhoAmIServerAction";
import WhoAmIAPI from "./WhoAmIAPI";
+import WhoAmIRSC from "./WhoAmIRSC";

export default async function TestRoute() {
  const session = await auth();

  return (
    <main>
      <h1 className="text-3xl mb-5">Test Route</h1>
      <div>User: {session?.user?.name}</div>
      <WhoAmIServerAction />
      <WhoAmIAPI />
+     <WhoAmIRSC />
    </main>
  );
}

動作確認

動作確認します。

$ pnpm run dev

ログインして保護ページにアクセスすると、先ほど作成したコンポーネントで取得したユーザー名が表示されます。

http://localhost:3000/protected

alt text

コミットします。

$ git add .
$ git commit -m "RSC を通して認証情報を取得"

まとめ

この記事では、Next.js App Router で Auth.js の V5 Beta を利用しました。

作業リポジトリ

作業リポジトリは以下になります。

https://github.com/hayato94087/next-auth-sample

Discussion