🛡️
Next.js のアプリに NextAuth でシンプルな認証機能をつける
背景
Next.js を使って社内向けのちょっとしたお遊び web アプリを作ってます。
一応社内の人間だけがアクセスできるようにしたく、最低限の認証機能を付けたいなと思ってライブラリを探していたところ、NextAuth.js にたどり着きました。
要件
- ID / Password を使ってログインできる
- 固定の ID / Password を設定して社内のみに共有する想定
 - なのでユーザー登録は不要
 
 - ログインしていない場合は、どのルーティングもログインページにリダイレクトされる
 
バージョン
| name | version | 
|---|---|
| next | 14.0.2 | 
| next-auth | 4.24.5 | 
インストール
pnpm i next-auth
beta で v5 も公開されていたのですが、今回は情報が豊富にあるであろう安定版の v4 を選択。
認証機能の準備
api に認証用の route を作成することで認証機能を利用できるようになる模様。
今回は ID / Pass を使ってのログインなので CredentialsProvider を使います。
src/app/api/auth/[...nextauth]/route.ts
import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: 'Ninjin Sirisiri',
      credentials: {
        id: {
          label: 'Id',
          type: 'text',
        },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        // credentials に入力が渡ってくる
        // id, password はここでベタ打ちして検証している
        const matched =
          credentials?.id === 'id' && 
          credentials?.password === 'password'
        if (matched) {
          // 今回は null を返さなければなんでもよいので適当
          return {
            id: '29472084752894723890248902',
          }
        } else {
          return null
        }
      },
    }),
  ],
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
認証の設定
今回はログイン以外のすべてのページに認証をかけます。
Next.js の middleware を使うことでシンプルに実装できました。
src/middleware.ts
export { default } from 'next-auth/middleware'
export const config = {
  // api と signin は未認証でも使いたいので弾く
  // _next は web フォントの読み込み等でも middleware が反応していたので除外してみた
  matcher: ['/((?!api|signin|_next).*)'],
}
ここまで設定すると、任意のページを開いたときに NextAuth が用意しているシンプルなログインページが表示されました。
さきほどの authOptions にベタ打ちした ID / Pass を入力するとログインに成功!

ちなみにログインした際の認証情報は cookie に保存される模様。
再度認証のテストをする場合は cookie を消せば OK。

ログインページのデザイン変更
このままだとログインページのデザインが NextAuth のデフォルトで味気ない。
デザインは任意でカスタマイズできるようです。
app directory 向けの記事としてこちらを参考に進めてみます。
option の追加
src/app/api/auth/[...nextauth]/route.ts
import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
export const authOptions: NextAuthOptions = {
  ...
+ pages: {
+   signIn: '/signin',
+ },
}
page の作成
スタイリングとかエラーハンドリングは一旦全部無視して、最低限の機能だけ実装してみました。
ポイントとしては以下かなと思いました。
- 
next-auth/reactから import したsignInメソッドを呼ぶとログイン処理を走らせられる - URL から受け取った callbackUrl を signIn に渡すことでログイン後のリダイレクトを制御できる
 
src/app/signin
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { signIn } from 'next-auth/react'
import { useState } from 'react'
export default function SignInPage() {
  return (
    <main>
      <LoginForm />
    </main>
  )
}
export const LoginForm = () => {
  const router = useRouter()
  const searchParams = useSearchParams()
  const [id, setId] = useState('')
  const [password, setPassword] = useState('')
  const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const callbackUrl = searchParams.get('callbackUrl') || '/'
    try {
      const response = await signIn('credentials', {
        redirect: false,
        id,
        password,
        callbackUrl,
      })
      if (response?.error) {
        console.log(response.error)
      } else {
        router.push(callbackUrl)
      }
    } catch (err) {
      console.log(err)
    }
  }
  return (
    <div>
      <h1>ログイン</h1>
      <form onSubmit={onSubmit}>
        <div>
          <label htmlFor="id">ID</label>
          <input
            required
            type="text"
            id="id"
            value={id}
            onChange={(event) => setId(event.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            required
            type="password"
            id="password"
            value={password}
            onChange={(event) => setPassword(event.target.value)}
          />
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  )
}
Discussion