[Next.js] App Router 時代の next-auth の使い方
はじめに
こんにちは、株式会社TERASSでエンジニアをしている myrear です。
つい先日 Next.js 14 のリリースがアナウンスされましたね。
Server Actions が安定版になるなどいくつかの変更がありますが、要点は既に解説している方がいらっしゃるのでそちらを参考にしていただければと思います。
その中でも筆者が気になったのが Next.js Learn の新しいコースで、 App Router に関するコースが新しく追加されました。
Next.js における認証といえば next-auth が一般的かと思いますが、 Pages Router との組み合わせは調べれば出てくるものの App Router との組み合わせがどうなるのかははっきりとしないままでした。
そこで今回 Next.js Learn に新しく追加されたチャプターである Chapter 15 Adding Authentication を参考にして App Router における認証にトライしてみます。
環境構築
Next.js Learn は本来チャプター1から順々にやっていくものです。
なので突然チャプター15だけ読んでもそれ以前のチャプターを履修済みの前提で話が進むのでいまいちとっかかりにくいです。
なのでチャプター15は参考程度に読むことにし、まずは Next.js が動く環境を作っていきます。
と言っても create-next-app
するだけです。
$ npx create-next-app@latest
Ok to proceed? (y) y
✔ What is your project named? … app-router-auth
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … Yes
✔ What import alias would you like configured? … @/*
プロジェクト名は何でもいいですがここでは app-router-auth
としました。
一旦ディレクトリ移動して動くことを確認します。
$ cd app-router-auth
$ npm run dev
http://localhost:3000 を開いてこんな画面になっていればOKです。
ページの追加
/dashboard
/login
の2つのルートを追加します。
/dashboard
がログイン後の遷移先、 /login
がログイン時の遷移先の役割になります。
中身は一旦適当で大丈夫です。
export default function Dashboard() {
return <div>dashboard</div>
}
export default function Login() {
return <div>login</div>
}
next-auth の設定
next-auth
を追加します。
$ npm i next-auth@beta
プロジェクトルートに .env
ファイルを以下の内容で作成します。
AUTH_SECRET=some-secret
AUTH_URL=http://localhost:3000/api/auth
AUTH_SECRET
は今回利用しないのですがないと next-auth
に怒られるので適当な値を入れておきます。
プロジェクトルートに next-auth
の設定ファイルを作成します。
import type { NextAuthConfig } from 'next-auth'
export const authConfig: NextAuthConfig = {
providers: [],
}
この設定ファイルに色々と追加していきます。
Credentials プロバイダの設定
今回の認証にはメールアドレスとパスワードの2つで行う Credentials プロバイダ を使用します。
簡単のため、メールアドレスは user@nextemail.com
かつパスワードは 123456
なら認可、それ以外は拒否します。
認可中の状態を目で追いやすくするために5000ミリ秒待つことにします。
import type { NextAuthConfig } from 'next-auth'
+ import Credentials from 'next-auth/providers/credentials'
export const authConfig: NextAuthConfig = {
- providers: [],
+ providers: [
+ Credentials({
+ async authorize(credentials) {
+ await new Promise((resolve) => setTimeout(resolve, 5000))
+
+ const email = 'user@nextemail.com'
+ return credentials.email === email && credentials.password === '123456'
+ ? { id: 'userId', email }
+ : null
+ },
+ }),
+ ],
}
リダイレクト先のページの設定
認証されていなければログインページに遷移させたい、みたいなケースはよくあると思います。
このような設定は pages
オプションから行えます。
import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const authConfig: NextAuthConfig = {
providers: [
// ...
],
+ pages: {
+ signIn: '/login',
+ },
}
ここではサインインが必要なときは /login
に遷移させるようにしました。
ルートの保護
ルートに対してアクセスがあったときに、認証されている/されていないでリダイレクトしたりみたいな処理を書いていきます。
import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
export const authConfig: NextAuthConfig = {
// ...
+ callbacks: {
+ authorized({ auth, request: { nextUrl } }) {
+ const isLoggedIn = !!auth?.user
+ const isOnDashboard = nextUrl.pathname.startsWith('/dashboard')
+ if (isOnDashboard) {
+ if (isLoggedIn) return true
+ return false
+ } else if (isLoggedIn) {
+ return Response.redirect(new URL('/dashboard', nextUrl))
+ }
+ return true
+ },
+ },
}
何やら少し複雑に見えますが、リダイレクトループを防ぎつつ、ログインしていれば /dashboard
に遷移するようにしているだけです。
ミドルウェアの作成
もろもろの設定を動作させるためのミドルウェアを作成します。
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'
export default NextAuth(authConfig).auth
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.png).*)'],
}
ミドルウェアの詳細について詳細には触れませんが、簡単に言うとミドルウェアが実行する関数とミドルウェアを適用する範囲を設定しています。
認証関連の関数の作成
認証をする上でログイン・ログアウト用の関数は欠かせません。
auth.ts
を作成します。
import NextAuth from 'next-auth'
import { authConfig } from '../auth.config'
export const { signIn, signOut } = NextAuth(authConfig)
これらの関数は基本的に Server Actions として呼ばれます。
認証フローの整備
設定は済んだので認証の流れを確認できるようにしていきます。
ログインフォームの実装
まずは認証するための Server Actions を作成します。
単純に <form action={signIn}>
としてしまうと結果がわからないので useFormState
に渡せるようなシグネチャにしてあげる必要があります。
'use server'
import { signIn } from '@/auth'
export async function authenticate(prevState: boolean, formData: FormData) {
try {
await signIn('credentials', Object.fromEntries(formData))
return true
} catch (error) {
if ((error as Error).message.includes('CredentialsSignin')) {
return false
}
throw error
}
}
Credentials プロバイダを使っているので signIn
関数の第1引数には 'credentials'
を指定します。
メールアドレスやパスワードが違っていた場合は Credentials プロバイダがそれ用のエラーをスローしてくれるのでそれだけキャッチし、ログインできたかどうかを判定可能にしています。
ログインページにログインフォームを追加します。
'use client'
import { authenticate } from '@/app/login/actions'
import { useFormState, useFormStatus } from 'react-dom'
export default function LoginForm() {
const [state, formAction] = useFormState(authenticate, true)
return (
<form action={formAction}>
<label>
メールアドレス:
<input className="text-gray-900" type="email" name="email" />
</label>
<label>
パスワード:
<input className="text-gray-900" type="password" name="password" />
</label>
{!state && (
<div className="text-red-500">
メールアドレスかパスワードが違います。
</div>
)}
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button aria-disabled={pending}>
{pending ? 'ログイン中' : 'ログインする'}
</button>
)
}
import LoginForm from '@/app/login/LoginForm'
export default function Login() {
return <LoginForm />
}
ダッシュボードの実装
/dashboard
にログアウト用のボタンを用意します。
import { signOut } from '@/auth'
export default function Dashboard() {
return (
<div>
<form
action={async () => {
'use server'
await signOut()
}}
>
<button>ログアウト</button>
</form>
dashboard
</div>
)
}
いざ認証
ここまできたらあとは動作確認するだけです。
npm run dev
で開発サーバを立ち上げ http://localhost:3000/dashboard にアクセスしてみましょう。
ログインページにリダイレクトされているはずです。
試しにログインに失敗してみると5秒間ログイン中になった後にエラーメッセージが表示されます。
正しいメールアドレスとパスワードを入力してみると、5秒間ログイン中になった後にダッシュボードに遷移します。
ログアウトボタンをクリックすると最初のログインページに戻ってきます。
お疲れ様でした。
おわりに
App Router での認証について見てきました。
カスタムフックで認証状態を取得していた Pages Router 時代と比べると簡単になったのかな?という印象を受けました(あまり詳しくはないですが)。
Server Actions も安定版になったので next-auth のベータが取れたらプロダクションでも使ってみたいですね。
コードの全体像は GitHub に置いてあります。
Discussion
next-auth@betaでは実装できるようですが、beta以前の4系(next-auth@4.x.y)とNext.js v14は互換性があるんでしょうか...?