Yamase Note
  • ホーム
  • チュートリアル
  • Next.js

Next.jsのチュートリアルをやってみる【認証の追加】

2024年11月18日2024年11月28日

SHARE

  • ポスト
  • シェア
  • はてブ
  • LINE

目次

Toggle
  • 前回
  • はじめに
  • 認証とは?
  • 認証と認可
  • ログインルートの作成
  • NextAuth.js
  • NextAuth.jsのセットアップ
    • NextAuth.jsのインストール
    • opensslのインストール
      • Visual C++ 2017再配布パッケージ のインストール
      • windowsOS用opensslのインストール
  • ページ・オプションの追加
  • NextAuth.jsのミドルウェアでルートを保護する
    • パスワードハッシュ
    • 認証情報プロパイダーの追加
    • サインイン機能の追加
    • ログインフォームの更新
    • ログアウト機能の追加
    • ログイン確認
  • まとめ
  • 次回

前回

2024年9月28日 Next.jsのチュートリアルをやってみる【アクセシビリティの向上編】

はじめに

前回は請求書ルートにあるフォームのバリデーションを追加してアクセシビリティを改善していきました。今回はダッシュボードに認証を追加していきます。

認証とは?

多くのウェブアプリケーションで認証機能は重要な役割を担っています。

安全なウェブサイトではユーザーの身元を確認するために複数の方法を使用することがよくあります。例えば、ユーザー名とパスワードを入力した後、サイトがユーザーのデバイスに認証コードを送信したり、Google Authenticatorのような外部アプリを使用したりします。たとえパスワードを知られたとしても、固有のトークンがなければアカウントにアクセスすることができなくなるので、この2要素認証(2FA)はセキュリティの強化に役立ちます。

認証と認可

ウェブ開発において、認証と認可は異なる役割を果たします。

認証ユーザーが本人であることを確認すること。ユーザー名とパスワードのようなもので本人であることを証明する。
認可認証のあとに行う。ユーザーの身元が確認された後、アプリケーションのどの部分の使用を許可するかを決める。

ログインルートの作成

まずは/loginというルートを作成して、以下のコードを追加しましょう。

/app/login/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
import LoginForm from '@/app/ui/login-form';
 
export default function LoginPage() {
  return (
    <main className="flex items-center justify-center md:h-screen">
      <div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
        <div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
          <div className="w-32 text-white md:w-36">
            <AcmeLogo />
          </div>
        </div>
        <LoginForm />
      </div>
    </main>
  );
}

<LoginForm />をインポートして、使用していることに気が付くと思います。

NextAuth.js

NextAuth.jsを使用してアプリケーションに認証を追加します。NextAuth.jsは、セッション管理、サインイン、サインアウトなど、認証に関わる複雑な作業の多くを抽象化してくれます。これらの機能を手動で実装することもできますが、時間がかかりミスが発生しがちです。NextAuth.jsはこれらのプロセスを簡素化し、認証のための統一された機能を提供します。

NextAuth.jsのセットアップ

NextAuth.jsのインストール

NextAuth.js をインストールするためにターミナルで以下のコマンドを実行しましょう。

pnpm i next-auth@beta

チュートリアルの時点ではbeta版が互換性があるとのことなので、合わせてBeta版をインストールしました。

opensslのインストール

チュートリアルではopensslコマンドを実行していますが、Windows環境では別途導入する必要があります。

Visual C++ 2017再配布パッケージ のインストール

opensslコマンドを実行するには事前にVisual C++ 2017 再配布パッケージをインストールする必要があります。

Microsoftの公式ページからダウンロードして、インストールしておきましょう。

windowsOS用opensslのインストール

公式サイトからWindows OS用のインストーラーをダウンロードしましょう。

続いてアプリケーションの秘密鍵を生成するために以下のコマンドを実行しましょう。このキーはクッキーを暗号化して、ユーザーセッションのセキュリティを確保するために使用されます。

openssl rand -base64 32

秘密鍵が生成されたらコピーして、.envファイルのAUTH_SECRET=に張り付けて保存する。

ページ・オプションの追加

プロジェクトのルートにauth.config.tsファイルを作成して、NextAuth.jsの設定オプションが含まれまれた、authConfigオブジェクトをエクスポートします。今のところ以下の様にpageオプションだけが格納されています。

/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
} satisfies NextAuthConfig;

pagesオプションを利用すると、カスタムサインイン、サインアウト、エラーページのルートを指定できます。必須ではありませんが、pagesオプションにsignIn: '/login'を追加することで、ユーザーはNextAuth.jsのデフォルトページではなく、カスタムログインページにリダイレクトすることができます。

NextAuth.jsのミドルウェアでルートを保護する

続いて、ルートを保護するロジックを追加します。ログインしていないユーザーがダッシュボードのページにアクセスできないようにしましょう。

/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  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;
    },
  },
  providers: [], // とりあえず空の配列でプロパイだを追加
} satisfies NextAuthConfig;

authorizedコールバックは、リクエストがNext.jsミドルウェア経由でページにアクセスすることを許可されているかどうかを確認するために使用されます。リクエストが完了する前に呼び出され、authプロパティとrequestプロパティを持つオブジェクトを受け取ります。authプロパティにはユーザーのセッションが含まれ、requestプロパティには入力されたリクエストが含まれます。

porviderオプションは、様々なログインオプションを列挙する配列です。現時点では、NextAuthの設定を満たすために空の配列を渡しています。詳細は後述します。

次は、authConfigオブジェクトをミドルウェアファイルにインポートする必要があります。プロジェクトのルートにmiddleware.tsというファイルを作成して、以下のコードを追加しましょう。

/middleware.ts
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$).*)'],
};

このコードではNextAuth.jsをauthConfigオブジェクトで初期化し、authプロパティをエクスポートしています。また、ミドルウェアのmatcherオプションを使用して、特定のパスで実行するように指定しています。

このタスクにミドルウェアを採用する利点はミドルウェアが認証を検証するまで保護されたルートはレンダリングを開始しないため、アプリケーションのセキュリティとパフォーマンスが向上することです。

パスワードハッシュ

パスワードをデータベースに保存する前にハッシュ化しましょう。葉shスカはパスワードを固定長の文字列に変換し、ランダムに見えるようにします。

seed.jsファイルではユーザーのパスワードをデータベースに保存する前のハッシュ化するためにvcryptというパッケージを使用しました。後ほどユーザーが入力したパスワードがデータベースのパスワードと一致するかを比較するために再度使用します。ただし、bcryptパッケージ用に別のファイルを作成する必要があります。これは、bcryptがNext.jsミドルウェアでは利用できないNode.js APIに依存しているためです。

新しくauth.tsファイルを作成してauthConfigオブジェクトを作成していきます。

/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
});

認証情報プロパイダーの追加

次はNextAuth.jsのprovidersオプションを追加しましょう。providersはGoogleやGitHubなどのログインオプションを列挙する配列です。今回はCredentials providerのみを使用して進めます。

Credentials provider はユーザーがユーザー名とパスワードでログインできるようにします。

/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [Credentials({})],
});

サインイン機能の追加

認証ロジックを処理するには、authorize関数を使用します。サーバー・アクションと同様にzodを使用してメールとパスワードを検証してから、そのユーザーがデータベースに存在するかどうかをチェックします。

/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);
      },
    }),
  ],
});

認証情報を検証した後、データベースからユーザーを問い合わせる新しいgetUser関数を作成します。

/auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
 
async function getUser(email: string): Promise<User | undefined> {
  try {
    const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);
 
        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;
        }
 
        return null;
      },
    }),
  ],
});

次に、bcrypt.compareを呼び出してパスワードが一致するかをチェックします。

/auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { sql } from '@vercel/postgres';
import { z } from 'zod';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
 
// ...
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        // ...
 
        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null;
          const passwordsMatch = await bcrypt.compare(password, user.password);
 
          if (passwordsMatch) return user;
        }
 
        console.log('Invalid credentials');
        return null;
      },
    }),
  ],
});

最後に、パスワードが一致すればユーザーを返し、一致しなければnullを返してユーザーがログインできないようにします。

ログインフォームの更新

続いて、認証ロジックをログインフォームに追加する必要があります。action.tsファイルにauthenticateという新しいアクションを作成します。このアクションは、auth.tsからsignIn関数をインポートする必要があります。

/app/lib/action.ts
'use server';
 
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
 
// ...
 
export async function authenticate(
  prevState: string | undefined,
  formData: FormData,
) {
  try {
    await signIn('credentials', formData);
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case 'CredentialsSignin':
          return 'Invalid credentials.';
        default:
          return 'Something went wrong.';
      }
    }
    throw error;
  }
}

CredentialsSigninエラーが発生した場合は適切なエラーメッセージを表示します。エラーはこちらを参照してください。

最後にlogin-form.tsxコンポーネントでReactのuseActionStateを使用して、サーバーアクションを呼び出し、フォームエラーを処理し、フォームの保留状態を表示することができます。

app\ui/login-form.tsx
'use client';
 
import { lusitana } from '@/app/ui/fonts';
import {
  AtSymbolIcon,
  KeyIcon,
  ExclamationCircleIcon,
} from '@heroicons/react/24/outline';
import { ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from '@/app/ui/button';
import { useActionState } from 'react';
import { authenticate } from '@/app/lib/actions';
 
export default function LoginForm() {
  const [errorMessage, formAction, isPending] = useActionState(
    authenticate,
    undefined,
  );
 
  return (
    <form action={formAction} className="space-y-3">
      <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
        <h1 className={`${lusitana.className} mb-3 text-2xl`}>
          Please log in to continue.
        </h1>
        <div className="w-full">
          <div>
            <label
              className="mb-3 mt-5 block text-xs font-medium text-gray-900"
              htmlFor="email"
            >
              Email
            </label>
            <div className="relative">
              <input
                className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
                id="email"
                type="email"
                name="email"
                placeholder="Enter your email address"
                required
              />
              <AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
            </div>
          </div>
          <div className="mt-4">
            <label
              className="mb-3 mt-5 block text-xs font-medium text-gray-900"
              htmlFor="password"
            >
              Password
            </label>
            <div className="relative">
              <input
                className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
                id="password"
                type="password"
                name="password"
                placeholder="Enter password"
                required
                minLength={6}
              />
              <KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
            </div>
          </div>
        </div>
        <Button className="mt-4 w-full" aria-disabled={isPending}>
          Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
        </Button>
        <div
          className="flex h-8 items-end space-x-1"
          aria-live="polite"
          aria-atomic="true"
        >
          {errorMessage && (
            <>
              <ExclamationCircleIcon className="h-5 w-5 text-red-500" />
              <p className="text-sm text-red-500">{errorMessage}</p>
            </>
          )}
        </div>
      </div>
    </form>
  );
}

ログアウト機能の追加

<SideNav />にログアウト機能を追加するには、<form>要素内でauth.tsのsignOut関数を呼び出します。

/ui/dashboard/sidenav.tsx
import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
import AcmeLogo from '@/app/ui/acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
import { signOut } from '@/auth';
 
export default function SideNav() {
  return (
    <div className="flex h-full flex-col px-3 py-4 md:px-2">
      // ...
      <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
        <NavLinks />
        <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
        <form
          action={async () => {
            'use server';
            await signOut();
          }}
        >
          <button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
            <PowerIcon className="w-6" />
            <div className="hidden md:block">Sign Out</div>
          </button>
        </form>
      </div>
    </div>
  );
}

ログイン確認

それでは、以下の認証情報を使用してログイン・ログアウトを確認してみましょう。
トップページからログインボタンを押下して以下のログインページに遷移します。ログイン後ダッシュボードページが表示されたら成功です。

  • メールアドレス: user@nextmail.com
  • パスワード: 123456

まとめ

  • NextAuth.js は認証回りの様々な機能を提供している
    • providersオプションはGoogleやGitHubなどログインオプションに使用する
  • パスワードはデータベースに平文ではなくハッシュ化してから保存する
  • bcryptパッケージはハッシュ化やパスワード比較に使用できる
  • opensslコマンドを使用して秘密鍵を生成することができる
    • windowsの場合はインストールが必要
  • パッケージによってはNode.js APIに依存しているため別ファイルに分離する必要がある

次回

2024年11月28日 Next.jsのチュートリアルをやってみる【メタデータの追加】
SHARE
  • ポスト
  • シェア
  • はてブ
  • LINE

CATEGORY :

  • Next.js
  • チュートリアル

TAGS :

  • authenticate
  • bcrypt
  • credentials
  • Hash
  • login
  • next-auth
  • Next.js

関連記事

  • Next.jsのチュートリアルをやってみる【エラー処理編】

    Next.jsのチュートリアルをやってみる【エラー処理編】

  • Next.jsのチュートリアルをやってみる【データの取得編】

    Next.jsのチュートリアルをやってみる【データの取得編】

  • Next.jsのチュートリアルをやってみる【ストリーミング編】

    Next.jsのチュートリアルをやってみる【ストリーミング編】

  • Next.jsのチュートリアルをやってみる【データベースの設定編】

    Next.jsのチュートリアルをやってみる【データベースの設定編】

この記事を書いた人
Yamase
雑食エンジニア

いろんな技術に目移りするエンジニア。 技術的な情報を中心にメモ代わりに発信しています。 もしかしたらゲームとか好きなことについて書いたり?

X

コメントを残す コメントをキャンセル

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

前の記事

Next.jsのチュートリアルをやってみる【アクセシビリティの…

次の記事

Next.jsのチュートリアルをやってみる【メタデータの追加】
プロフィール背景画像
プロフィール画像

Yamase

いろんな技術に目移りするエンジニア。 技術的な情報を中心にメモ代わりに発信しています。 もしかしたらゲームとか好きなことについて書いたり?

最近の投稿

  • 個人的に検討したGo言語ライブラリ
  • Windows11にDocker環境を構築する方法
  • Next.jsのチュートリアルをやってみる【メタデータの追加】
  • Next.jsのチュートリアルをやってみる【認証の追加】
  • Next.jsのチュートリアルをやってみる【アクセシビリティの向上編】
コンテンツ一覧
  • Home
  • 記事のトップへ
  • Twitter

カテゴリー

  • Go
  • WordPress
  • チュートリアル
    • Go
    • Next.js
  • 備忘録
  • 勉強
    • LeetCode
  • 未分類
  • 開発

目次Toggle Table of ContentToggle

  • 前回
  • はじめに
  • 認証とは?
  • 認証と認可
  • ログインルートの作成
  • NextAuth.js
  • NextAuth.jsのセットアップ
    • NextAuth.jsのインストール
    • opensslのインストール
      • Visual C++ 2017再配布パッケージ のインストール
      • windowsOS用opensslのインストール
  • ページ・オプションの追加
  • NextAuth.jsのミドルウェアでルートを保護する
    • パスワードハッシュ
    • 認証情報プロパイダーの追加
    • サインイン機能の追加
    • ログインフォームの更新
    • ログアウト機能の追加
    • ログイン確認
  • まとめ
  • 次回

同カテゴリーの人気記事

  • 1
    【Meta Quest 3開発】QuestLinkを使用してもUnityのPlayモードでアプリが起動しない
    【Meta Quest 3開発】QuestLinkを使用してもUnityのPlayモードでアプリが起動しない
  • 2
    【Meta Quest 3開発】HMD・手・コントローラーのトラッキング
    【Meta Quest 3開発】HMD・手・コントローラーのトラッキング
  • 3
    【C#】CancellationTokenの使い方
    【C#】CancellationTokenの使い方
  • 4
    【Meta Quest 3開発】環境構築とプロジェクトの設定
    【Meta Quest 3開発】環境構築とプロジェクトの設定
  • 5
    【C#】CancellationTokenSourceの使い方
    【C#】CancellationTokenSourceの使い方
プロフィール背景画像
プロフィール画像

Yamase

いろんな技術に目移りするエンジニア。
技術的な情報を中心にメモ代わりに発信しています。
もしかしたらゲームとか好きなことについて書いたり?

カテゴリー

  • Go
  • Go
  • LeetCode
  • Next.js
  • WordPress
  • チュートリアル
  • 備忘録
  • 未分類
  • 開発

タグ

Array BFS C# Coding command Database Develop DFS DP Error Fetch Fuzzing Go golang Hash HashSet interface LeetCode LinkedList MetaQuest3 Meta XR SDK module Next.js PostgreSQL Query Queue React Rendering REST RESTful router server Skeleton SQL Streaming Tutorial TypeScript Unity VR Zod アルゴリズム 二分木 動的計画法 深さ優先探索 部分列

ランキング

  • 1
    【Meta Quest 3開発】QuestLinkを使用してもUnityのPlayモードでアプリが起動しない
    【Meta Quest 3開発】QuestLinkを使用してもUnityのPlayモードでアプリが起動しない
  • 2
    【Meta Quest 3開発】HMD・手・コントローラーのトラッキング
    【Meta Quest 3開発】HMD・手・コントローラーのトラッキング
  • 3
    【C#】CancellationTokenの使い方
    【C#】CancellationTokenの使い方
  • 4
    【Meta Quest 3開発】環境構築とプロジェクトの設定
    【Meta Quest 3開発】環境構築とプロジェクトの設定
  • 5
    【C#】CancellationTokenSourceの使い方
    【C#】CancellationTokenSourceの使い方
HOME

© 2025 Seiya Yamaguchi All rights reserved.