🌟

Next.js +NextAuth.js + oauth(GitHub) で認証機能を作る

2024/02/24に公開

はじめに

この記事では、Next.js v14とNextAuth.jsを使って、GitHubを認証プロバイダーとするOAuth認証を行うページを作成します。
NextAuth.jsはV5を使いますが、現在ベータ版のため今後変更される可能性があります。

作ったソースは以下にコミットされています。

https://github.com/0701pino/next-auth-sample

使用するバージョン

  • Next.js: 14.1.0
  • NextAuth: 5.0.0-beta.11

プロジェクトのセットアップ

このセクションでは、既に作成されたNext.jsプロジェクトにNextAuthを導入し、OAuth認証のための環境設定を行います。

NextAuthのインストール

NextAuthをプロジェクトに追加するには、yarnを使用してパッケージをインストールします。ターミナルで以下のコマンドを実行してください。

yarn add next-auth@beta

GitHub OAuthアプリの設定

GitHub認証を利用するためには、まずGitHubにOAuthアプリケーションを登録し、必要なクライアントIDとクライアントシークレットを取得する必要があります。

アプリの登録

  1. GitHubにログインした状態で、GitHubの設定ページにアクセスします

  2. 「Register a new application」ボタンを押します

  3. アプリケーション登録フォームに必要情報を入力します

    • Application name: アプリケーションの名前を入力します
    • Homepage URL: アプリケーションのホームページURLを入力します。ローカル開発の場合はhttp://localhost:3000とします
    • Application description (任意): アプリケーションの説明を入力します
    • Authorization callback URL: 認証後にGitHubがリダイレクトするURLを入力します。NextAuthを使用する場合、http://localhost:3000/api/auth/callback/githubとします。
    • Enable Device Flow: デバイスフローで認可する場合にチェックを入れます。今回はチェックを入れません。

  1. 「Register application」ボタンを押すと、アプリケーションが作成されます。
  2. Client IDとClient secretメモします。
    Client secretは、「Generate a client secret」を押すと作成されます。Client secretは画面遷移すると二度と表示されないのでコピーボタンを押して確実に残しておきます。
    これらは、環境変数AUTH_GITHUB_IDAUTH_GITHUB_SECRETに設定します。

プロジェクトのセットアップ

NextAuth.jsをインストールし、OAuth認証のための環境設定を行います。

NextAuthのインストール

NextAuthをプロジェクトに追加するには、yarnを使用してパッケージをインストールします。ターミナルで以下のコマンドを実行してください。

yarn add next-auth@beta

@betaタグを指定することで、NextAuth v5のベータ版をインストールします。
この記事の作成時点では、5.0.0-beta.11でした。

環境変数の設定

NextAuthとGitHub OAuth認証を使用するには、いくつかの環境変数を設定する必要があります。
設定が必要なのは、GitHubから取得したクライアントIDとシークレット、セッションを管理するための秘密鍵です。
OAuthプロバイダーの場合、環境変数名は、AUTH_{PROVIDER}_{ID|SECRET}です。
セッションを管理するための秘密鍵の環境変数名は、AUTH_SECRETです。

プロジェクトのルートに.env.localファイルを作成し、以下のように環境変数を追加します。

AUTH_GITHUB_ID=GitHubクライアントID
AUTH_GITHUB_SECRET=GitHubシークレット
AUTH_SECRET=ランダムな長い文字列

AUTH_GITHUB_ID と AUTH_GITHUB_SECRET は、上でメモしておいた値を使用します。

AUTH_SECRETは、ランダムな文字列を設定するので、以下のコマンドを実行したり、https://generate-secret.vercel.app/32にアクセスしてランダムな文字列を取得して設定します。

openssl rand -base64 32

NextAuthの設定

lib/auth.ts の設定

どのようなソーシャルログインを追加するかは、auth.tsで設定します。
初期のベータ版に比べて記述が簡単になっています。
middlewareを使う場合は、これにcallbackを追加します。
下にあるmiddlewareの項目を参照してください。

lib/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
});

api/auth/[...nextauth]/route.tsの設定

NextAuth用apiをapp/api/auth/[...nextauth]/route.tsに作成します。
これも、初期のベータ版に比べて記述が簡単になっています。

ここでは、auth.tsを読み込んで、exportします。
なぜ、このような構成になっているかですが、route.tsでは、exportできるのが、HTTP methodに制限されているからです。

以下に解説されています。

app/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";

export const { GET, POST } = handlers;

route.tsによって、以下のリクエストを受けることができるようになります。

GET /api/auth/signin
POST /api/auth/signin/:provider
GET/POST /api/auth/callback/:provider
GET /api/auth/signout
POST /api/auth/signout
GET /api/auth/session
GET /api/auth/csrf
GET /api/auth/providers

middleware.ts の設定

middlewareとauthを組み合わせることで、認証していなくては表示されないページを作ることができます。
matcherでマッチするURLでmiddlewareは動作します。

src/middleware.ts
export { auth as middleware } from "@/lib//auth";

// いくつかのパスではmiddlewareを動作させない
// macherは、以下で始まるものを除くすべてのリクエスト・パスにマッチする:
// - api (APIルート)
// - _next/static (静的ファイル)
// - _next/image (画像最適化ファイル)
// - favicon.ico (ファビコンファイル)
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

auth.tsには、callbacksを設定します。
ここでは、/dashboardのときに認証状態を返却しています。
その他の場合はtrueを返却しています。
これがmiddlewareのmatcherと組み合わされて評価され、/dashboardにアクセスしたときに認証されていない場合は認証画面が表示されます。
もし、サイト全体を認証していなければ表示されないようにするには、pathnameをチェックせずに、認証状態だけを返却すれば実現できます。

lib/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  callbacks: {
    authorized({ request, auth }) {
      const { pathname } = request.nextUrl;
      if (pathname === "/dashboard") {
        return !!auth;
      }
      return true;
    },
  },
});

signIn,signOutボタンコンポーネント

GitHubを使ったサインイン、サインアウト用ボタンは、auth()でセッション情報を取得して処理を切り替えます。

src/components/user-button.tsx
import { auth, signIn, signOut } from "@/lib/auth";

export default async function UserButton() {
  const session = await auth();
  return (
    <>
      {!session && (
        <form
          action={async () => {
            "use server";
            await signIn("GitHub");
          }}
        >
          <button className="my-5 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">
            Sign in with GitHub
          </button>
        </form>
      )}
      {session && (
        <form
          action={async () => {
            "use server";
            await signOut();
          }}
        >
          <button className="my-5 rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700">
            Sign out
          </button>
        </form>
      )}
    </>
  );
}

サーバーコンポーネント

サーバーコンポーネント内で認証状態を取得するには、auth()関数を使用してセッション情報を取得します。

src/components/server-component.tsx
import { Session } from "next-auth";

import { auth } from "@/lib/auth";

export default async function ServerComponent() {
  console.log("server component");
  const session: Session | null = await auth();

  return (
    <>
      <div className="text-xl"> server component</div>
      {session ? <div>user: {session.user?.name || "Guest"}</div> : <div>user: not signed in</div>}
    </>
  );
}

クライアントコンポーネント

サーバーコンポーネントで、認証状態を取得する場合、以前はuseSessionを使っていました。
しかしながら、https://authjs.dev/reference/nextjs/react#usesessionを見ると以下のように書いているので、App Routerでは使わないようです。

You will likely not need useSession if you are using the Next.js App Router (app/).

では、何を使うのかということは書いていませんでした。

そこで、fetchを使う方法とpropsでサーバーコンポーネントから設定する方法で実現してみました。

fetchを使う方法

fetchでセッション情報を取得して使います。

src/components/client-component.tsx
"use client";

import { useEffect, useState } from "react";

import { Session } from "next-auth";

export default function ClientComponent() {
  console.log("client component");
  const [session, setSession] = useState<Session | null>(null);
  useEffect(() => {
    async function fetchSession() {
      const res = await fetch("/api/auth/session");

      const sessionData = await res.json();
      console.log("sessionData", sessionData);
      setSession(sessionData);
    }

    fetchSession();
  }, []);

  return (
    <div className="mt-5">
      <div className="text-xl"> client component</div>
      {session ? <div>user: {session.user?.name || "Guest"}</div> : <div>user: not signed in</div>}
    </div>
  );
}

propsを使う方法

クライアントコンポーネントを呼び出す側でセッション情報をセットします。

src/components/client-component2.tsx
"use client";

import { Session } from "next-auth";

export default function ClientComponent2({ session }: { session: Session | null }) {
  console.log("client component2");

  return (
    <div className="mt-5">
      <div className="text-xl"> client component2</div>
      {session ? <div>user: {session.user?.name || "Guest"}</div> : <div>user: not signed in</div>}
    </div>
  );
}

画面サンプル

サインインしていない場合は、以下の表示になります。

サインインしている場合は、以下の表示になります。

まとめ

NextAuth.js V5ベータ11を使用したサンプルを紹介しました。
作ったソースは以下にコミットされています。

https://github.com/0701pino/next-auth-sample

参考

Discussion