【NextAuth】Credentials ProviderでシンプルなID/パスワード認証システム

Credentials Providerとは

Credentials Providerは、NextAuthで独自のユーザー名/パスワード認証を実装するためのプロバイダーです。

Credentials Provider の認証フロー ログインフォーム Credentials Provider (authorize関数) データベース 処理ステップ: 1. ユーザーがフォームに入力 – email – password 2. Credentials Providerが認証処理を実行 – 入力値のバリデーション – データベースでユーザー検索 – パスワードの照合 3. 認証結果 – 成功: JWTトークン生成&セッション作成 – 失敗: エラーメッセージを返却

データベースの準備

DB準備し.envファイルを編集

データベース接続設定 (.env)

DATABASE_URL="mysql://ユーザー名:パスワード@ホスト:3306/データベース名"

今回はPrisma不使用

ORM (Object-Relational Mapping)

SQLを直接書くことなく、オブジェクトのメソッドでDB操作ができる
SQL文詳しくなくてもDB操作ができる

  1. PHP APIを作成した理由:
    • さくらインターネットのデータベースにアクセスするため
    • 外部からの直接接続ができないため、PHPを介してアクセス
  2. Prismaは:
    • データベースに直接接続する必要がある
    • さくらインターネットの制限により使用が難しい
    • PHP APIの作成はPrismaとは無関係

これからの進め方:

  1. PHP APIを使用してデータベース操作を行う
  2. Next.jsからそのAPIを呼び出す
  3. NextAuthの実装を進める

NextAuthの設定

NextAuthのインストール

npm install next-auth

認証APIルートの作成 (src/app/api/auth/[...nextauth]/route.ts)

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: "Email & Password",
      credentials: {
        email: { 
          label: "メールアドレス", 
          type: "email",
          placeholder: "email@example.com"
        },
        password: { 
          label: "パスワード", 
          type: "password" 
        }
      },
      async authorize(credentials) {
        try {
          // PHP APIを呼び出してユーザー認証
          const res = await fetch("https://あなたのドメイン/auth-api.php", {
            method: 'POST',
            body: JSON.stringify({
              email: credentials?.email,
              password: credentials?.password
            }),
            headers: { "Content-Type": "application/json" }
          });

          const user = await res.json();

          if (user.success) {
            return {
              id: user.data.id,
              name: user.data.name,
              email: user.data.email
            }
          }
          return null;
          
        } catch (error) {
          console.error("Auth Error:", error);
          return null;
        }
      }
    })
  ],
//  pages: {
//    signIn: '/auth/signin',  // カスタムログインページのパス
//  }
});

export { handler as GET, handler as POST };

認証用のPHP API作成 (auth-api.php)

<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');

// POSTデータの取得
$json = file_get_contents('php://input');
$data = json_decode($json, true);

// データベース接続情報
$host = 'xxxx';
$dbname = 'xxxx';
$user = 'xxxx';
$pass = 'xxxx';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND password = ? LIMIT 1');
    $stmt->execute([$data['email'], $data['password']]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($user) {
        echo json_encode([
            'success' => true,
            'data' => [
                'id' => $user['id'],
                'email' => $user['email'],
                'name' => $user['name']
            ]
        ]);
    } else {
        echo json_encode([
            'success' => false,
            'error' => 'Invalid credentials'
        ]);
    }
} catch(PDOException $e) {
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ]);
}
?>

特定のページのみを認証必須にする

認証状態を確認するためのミドルウェアを作成

src/middleware.tsを作成
import { withAuth } from "next-auth/middleware"

// 特定のパスのみ認証を必須にする
export default withAuth({
  // ここで指定したパスのみ認証が必要
  pages: {
    signIn: "/api/auth/signin",
  }
})

// 認証が必要なパスを指定
export const config = {
  matcher: ["/nextauth-demo/:path*"]
}

デモページの作成

src/app/nextauth-demo/page.tsx
'use client';
import { useSession, signOut } from "next-auth/react"  // signOutをインポート
import { redirect } from "next/navigation"

export default function ProtectedPage() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div>Loading...</div>
  }

  if (status === "unauthenticated") {
    redirect("/api/auth/signin")
  }

  // ログアウト処理の追加
  const handleLogout = async () => {
    await signOut({ 
      callbackUrl: '/nextauth-demo'  // ログアウト後のリダイレクト先
    })
  }

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">認証が必要なページ</h1>
      <p className="mb-4">ようこそ {session?.user?.email ?? 'ゲスト'} さん</p>
      
      {/* ログアウトボタン追加 */}
      <button 
        onClick={handleLogout}
        className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
      >
        ログアウト
      </button>
    </div>
  )
}
/nextauth-demo/layout.tsx を作成
'use client';

import { SessionProvider } from "next-auth/react";

export default function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  );
}