🛡️

Next.js のアプリに NextAuth でシンプルな認証機能をつける

2024/02/14に公開

背景

Next.js を使って社内向けのちょっとしたお遊び web アプリを作ってます。
一応社内の人間だけがアクセスできるようにしたく、最低限の認証機能を付けたいなと思ってライブラリを探していたところ、NextAuth.js にたどり着きました。

https://next-auth.js.org/

要件

  • ID / Password を使ってログインできる
    • 固定の ID / Password を設定して社内のみに共有する想定
    • なのでユーザー登録は不要
  • ログインしていない場合は、どのルーティングもログインページにリダイレクトされる

バージョン

name version
next 14.0.2
next-auth 4.24.5

インストール

pnpm i next-auth

beta で v5 も公開されていたのですが、今回は情報が豊富にあるであろう安定版の v4 を選択。

認証機能の準備

https://next-auth.js.org/getting-started/example

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 }

認証の設定

https://next-auth.js.org/configuration/nextjs#middleware

今回はログイン以外のすべてのページに認証をかけます。
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 のデフォルトで味気ない。
デザインは任意でカスタマイズできるようです。

https://codevoweb.com/nextjs-use-custom-login-and-signup-pages-for-nextauth-js/

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