Next.js で Auth.js v5 Beta を利用する
はじめに
この記事では Next.js App Router で Auth.js の V5 Beta を利用します。
具体的には、以下の動画で紹介されている内容を少し修正しながら実装していきます。
Auth.jsとは❓
Auth.js とは認証機能を実装できるライブラリです。以前は NextAuth とも呼ばれていました。
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
の内容を以下のように上書きします。
@tailwind base;
@tailwind components;
@tailwind utilities;
初期ページの初期化
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
を上書きします。
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の設定を上書きします。
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の設定を上書きします。
{
"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"]
}
スクリプトを追加
型チェックのスクリプトを追加します。
{
"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
.env
に AUTH_SECRET
を追加します。
$ touch .env
AUTH_SECRET=secret
.gitignore に .env
を追加します。
+.env
設定ファイルを作成
Auth.js の設定ファイルを作成します。このファイルで Auth.js がどの様に動作するかを設定します。
$ mkdir -p src/auth
$ touch 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
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
ページを修正
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
サインインするには、以下のリンクからサインインページに移動します。
サインインするとセッション情報が表示されます。
サインアウトするには、以下のリンクからサインアウトページに移動します。
サインアウトするとセッション情報が表示されません。
コミットします。
$ git add .
$ git commit -m "Auth.js をインストール、設定ファイルを作成、認証APIを作成、ページを修正"
サインイン、サインアウトボタンを追加
ヘルパー関数を作成し、signin
と signout
をエクスポートします。
$ touch 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
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>
);
}
"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>
);
}
ページに反映します。
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
ホームにアクセスすると、サインインボタンを表示されます。サインインボタンをクリックすると、サインインページに移動します。
ユーザー名とパスワードを入力してサインインします。
サインインすると、サインアウトボタンが表示されます。
コミットします。
$ git add .
$ git commit -m "サインイン、サインアウトボタンを追加"
保護ページを作成
サインインしていないとアクセスできないページを作成します。
保護ページを作成
まずページを作成します。
$ mkdir -p src/app/protected
$ touch 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
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
ログインしていない場合、保護ページにアクセスすると、サインインページにリダイレクトされます。
ログインしている場合は、保護ページにアクセスできます。
コミットします。
$ git add .
$ git commit -m "保護ページを作成"
Server Actions の追加
Server Actions を利用して認証情報を取得する方法を追加します。
Server Action を作成
Server Action を実装します。シンプルにユーザー名を返すだけの関数を作成します。
$ mkdir -p src/app/actions
$ touch 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
"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 をページに追加
ページを修正します。
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 で取得したユーザー名が表示されます。
コミットします。
$ git add .
$ git commit -m "Server Actions の追加"
クライアントコンポーネントから認証情報を取得
ここでは、クライアントコンポーネントから API を通して認証情報を取得する方法を追加します。
API を作成
API を作成します。API はサインインしているユーザーの名前を返します。
$ mkdir -p src/app/api/whoami
$ touch 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
"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>;
}
ページを修正
ページを修正します。
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
ログインして保護ページにアクセスすると、作成したコンポーネントで取得したユーザー名が表示されます。
コミットします。
$ git add .
$ git commit -m "API を通して認証情報を取得"
RSCから認証情報を取得
ここでは、RSC から API を通して認証情報を取得する方法を追加します。
RSCを作成
RSC を作成し、API を通して認証情報を取得します。
$ touch 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>;
}
ページを修正
ページを修正します。
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
ログインして保護ページにアクセスすると、先ほど作成したコンポーネントで取得したユーザー名が表示されます。
コミットします。
$ git add .
$ git commit -m "RSC を通して認証情報を取得"
まとめ
この記事では、Next.js App Router で Auth.js の V5 Beta を利用しました。
作業リポジトリ
作業リポジトリは以下になります。
Discussion