🔐

【Next.js】NextAuth.js、tRPC、Prismaを用いた認証システムの構築

2024/08/31に公開

はじめに

現代のWeb開発において、フロントエンドやバックエンドでTypeScriptを活用し、型安全なアプリケーションを構築することが求められるようになっています。その中で、注目を集めているのが「T3 Stack」という技術スタックです。効率的に型安全なアプリケーションを構築できることから、T3 Stackでの開発を推進しています。

T3 Stackとは

T3 Stackとはsimplicity(簡潔さ)、modularity(モジュール性)、full-stack type safety(フルスタックの型安全)を追求した思想に焦点を当てています。
そしてそれらを実現するために以下6つの技術スタックが採用されています。
✅ Next.js
✅ tRPC
✅ NextAuth.js
✅ Prisma
✅ Tailwind CSS
✅ Typescript

今回は、その中でも認証システムに焦点を当て、NextAuth.jsとtRPCとPrismaの連携による効率的な認証システムの構築についての内容です。以下の項目に沿って、解説していきます。

  • 1.NextAuth.jsについて
  • 2.App Routerでの実装
  • 3.tRPCとの連携
  • 4.Prismaとの連携

1.NextAuth.jsについて

NextAuth.jsは、Next.jsアプリケーション向けの、シンプルで使いやすい認証ライブラリです。
執筆時点でv4が安定版として、最新のv5がβ版としてリリースされています。この記事では、v4について詳しく紹介し、一部v4とv5の違いにも触れていきたいと思います。
https://next-auth.js.org/

サポートしている認証サービス

NextAuth.jsは以下の認証がサポートされています。

  • OAuth:OAuth 2.0を使用した認証方法で、Google、GitHub、Facebookなどのサードパーティプロバイダーを利用して認証を行います。
  • Email:ユーザーのメールアドレスに一時的なリンクを送信し、そのリンクをクリックすることで認証を完了する方法です。
  • Credentials:ユーザー名とパスワードを入力して独自のロジックで行う認証です。

中でもOAuthはアプリケーション側でパスワードを管理する必要なく、大手プロバイダーが提供する高いセキュリティの認証を利用できるため、非常に安全です。また、実装がシンプルであるため、認証以外の開発に集中でき、効率的にアプリケーションを構築することができるのでおすすめです。

利用可能な認証プロバイダー

OAuthで利用可能なプロバイダーを一部紹介します。これ以外にも約80ものプロバイダーを利用できます。

  • Google
  • Facebook
  • GitHub
  • LINE
  • Discord
  • Zoom
  • Amazon

https://next-auth.js.org/configuration/providers/oauth#built-in-providers

ログインの永続化方法

ログインの永続化の方法は2パターンあります。

①JWT(JSON Web Token)を使用してCookieに保存する

ログインの永続化のために認証情報をデータベースに保存するかは任意です。NextAuth.jsはOAuthを使用することでデータベース不要でも認証を利用できます。
ユーザーがログインすると、サーバーはユーザー情報をもとにJWTを生成します。JWTは暗号化されているため改ざんされにくく、サーバー側でユーザー情報を保持する必要がありません。
生成されたJWTは、ユーザーのブラウザにあるCookieに保存され、再びサイトを訪れたときに自動的にサーバーに送信されます。

②データベースアダプターを使用してデータベースに保存する

データベースに保存する場合はNextAuth.jsが提供しているデータベースアダプターを利用します。
データベースアダプターとは認証関連のデータ(ユーザー情報、セッション、アカウント、認証トークンなど)をデータベースに保存・管理するためのインターフェースです。これにより、データベース連携を非常に簡単に行うことができます。以下のような複数のデータベースのアダプターが提供されています。

  • Prisma
  • Drizzle ORM
  • Supabase
  • Firebase
  • DynamoDB

認証関連のデータを保存・管理するためにNextAuth.jsが期待する以下のようなデータ構造でテーブルを作成する必要があります。使用するアダプターによってはテーブルを自動で作成できるものも存在します。以下はデータベースのスキーマがどのような形式になるか公式が提供しているER図です。

NextAuth.js v4と v5の違い

NextAuth.jsは、もともとNext.jsに特化した認証ライブラリとして開発されましたが、ExpressやSvelteKitなどの他のJavaScriptライブラリでも利用できるようにAuth.jsとしてリリースされました。Next.jsと統合して使用する場合には、現在でもNextAuth.jsと呼ばれています。さらに2024年には、NextAuth.jsのv5がリリースされました。
執筆現在ではβ版ですが、Vercelの出しているNext.jsのexampleLearn Next.jsでもβ版が使用されていることから先に習得しておいて損はないかと思います。

v5の主な変更点

いくつかの変更点がある中で押さえておけば良さそうなものを挙げてみました。

  • ①App Routerファーストな設計(Pages Routerも引き続きサポート)
  • authの登場
  • ③設定ファイルの在り方
①App Routerファーストな設計

NextAuth.jsのv4のドキュメントにはPages Routerでの使い方が記載されており、App Routerの使い方の記載が使い方の記載がありませんでした。
しかし、NextAuth.jsのv5ではApp Routerファーストな設計で作られており、ドキュメントにもApp Routerでの使い方が記載されています。

authの登場

セッションの取得や認証に関わる機能を包括的に扱うようになったauthが登場しました。
これにより、これまでgetServerSessionwithAuthgetTokenが行っていたセッションの取得や認証に関わる機能が、authに統合されました。
どのように置き換えられたかは表の通りです。

使用場所 v4 v5
Server Components getServerSession auth
Middleware withAuth auth
Client Components useSession useSession
Route Handler - auth
③設定ファイルの在り方

One of our goals was to avoid exporting your configuration from one file
and passing it around as authOptions throughout your application.
訳:私たちのゴールのひとつは、ひとつのファイルから設定をエクスポートして、アプリケーション全体でauthOptionsとして受け渡すことを避けることでした。

ドキュメントにある通り開発者たちは、設定ファイルを1つのファイルからexportしてアプリケーション全体に渡す形を避けることを目指していました。そのため、v5では設定オプションを直接exportするのではなく、必要なメソッドをexportする形式に変更されました。

v4の設定例
v4では設定ファイル内でauthOptionsオブジェクトを作成し、それを使ってNextAuthを呼び出し、生成されたハンドラーをexportしていました。

app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";

const authOptions = {
  providers: [
  ],
  // その他の設定オプション
};

export const handler = NextAuth(authOptions);

v5の設定例
v5では、設定ファイルをリポジトリのルートに移動させ、authsignInsignOuthandlersなどのメソッドをexportする形式になりました。

auth.ts
import NextAuth from "next-auth"

const authOptions = {
  providers: [
  ],
  // その他の設定オプション
};

// メソッドをexport
export const { auth, handlers, signIn, signOut } = NextAuth(authOptions)

これにより、Route Handlerや他のコンポーネントで設定オプションをimportする必要がなくなり、exportされたメソッドを直接利用することができるようになりました。

app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers

また、セッション情報を取得する際も、v4ではauthOptionsをインポートして渡す必要がありましたが、v5では新しく追加されたauthを使用することで、これが不要になりました。
v4の設定例

app/page.tsx
import { authOptions } from "app/api/auth/[...nextauth]/route.ts"
import { getServerSession } from "next-auth/next"
 
export default async function Page() {
  const session = await getServerSession(authOptions)
  return (<p>Welcome {session?.user.name}!</p>)
}

v5の設定例

app/page.tsx
// 不要になった
//import { authOptions } from "app/api/auth/[...nextauth]/route.ts"
import { auth } from "@/auth"
 
export default async function Page() {
  const session = await auth()
  return (<p>Welcome {session?.user.name}!</p>)
}

2.App Routerでの実装

NextAuth.js v4のドキュメントはPages Routerで書かれていますが、App Routerで実装することもできます。この項ではNextAuth.js v4を用いてApp Routerで実装する際の方法や仕組みについて解説します。インストール等は省くので公式ドキュメントを参照してください。

NextAuth.jsが自動で認証を行ってくれるために必要な手順

まずは、NextAuth.jsが自動で認証を行ってくれるまでに必要な手順①②を解説します。

①認証に関する様々な設定を定義

NextAuth.jsの認証を利用するためにNextAuth関数に渡す設定オブジェクトを作成します。
このオブジェクトに認証に関する様々な設定を定義します。

auth.ts
export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    session: ({ session, user }) => ({
      ...session,
      user: {
        ...session.user,
        id: user.id,
      },
    }),
  },
  adapter: PrismaAdapter(db) as Adapter,
};

よく使われるオプションの一部を紹介します。

  • providers(必須):OAuthやCredentialsなど、ユーザーがサインインするための認証プロバイダーを指定します。複数のプロバイダーを設定することができます。
  • callbacks:認証フローの各ステップ(サインイン時やセッション作成時など)で実行されるカスタムロジックを設定するためのオプションです。
    たとえば、ユーザーがサインインした直後に、追加のチェックを行ったり、セッションにカスタムデータを追加ししたりすることができます。

  • adapter
    データベースアダプターを指定します。これにより、ユーザーやセッション情報をデータベースに保存することができます。

②認証に関するAPIリクエストを処理するハンドラーの設定

設定を定義したオブジェクトをNextAuth関数に渡し、app/api/auth/[...nextauth]/route.tsでexportします。
これは、Next.jsのRoute Handlersを使用しており、認証に関するリクエストを簡単に処理するために必要です。このディレクトリ名の[...nextauth]はNext.jsのcatch-all-segmentsというルーティングの一つです。

app/api/auth/[...nextauth]/route.ts
import { authOptions } from "@/server/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

App Routerのroute.tsファイルは、指定されたパスに対するリクエストをどのように処理するかを定義します。そのため、このディレクトリ構成と実装により、以下のような /api/auth/*のパスに対するGETおよびPOSTリクエストが自動的に認証されるということです。

  • /api/auth/signin:サインインページを表示し、ユーザーに利用可能な認証プロバイダーを選択させます。
  • /api/auth/signout:サインアウトページを表示し、現在のセッションを無効にし、ユーザーをサインアウトさせます。
  • /api/auth/callback/google:OAuthプロバイダーからのコールバックを処理し、ユーザーを認証してセッションを作成します。
  • /api/auth/session:現在のユーザーのセッション情報を取得します。

NextAuth.jsの機能を利用する(サインイン、サインアウト、セッション取得)

①、②でNextAuth.jsが自動的に認証処理を行なってくれるための準備は完了です。
必要に応じてサインイン、サインアウトボタンを設置したり、セッションを取得したりして使います。

サインイン、サインアウト

signInsignOutメソッドはクライアントサイドで呼び出して使うことができます。
signInを引数なしで呼び出すとNextAuth.jsのサインインページ(OAuthプロバイダーの一覧)に遷移し、プロバイダーを選択すると自動でサインインされ、セッション情報が付与されます。

app/page.tsx
import { signIn } from "next-auth/react";

export default async function Home() {
  return <button onClick={() => signIn()}>Sign in</button>;
}

signOutメソッドを呼び出すと自動でサインアウトしセッション情報が削除されます。

app/page.tsx
import { signOut } from "next-auth/react";

export default async function Home() {
  return <button onClick={() => signOut()}>Sign out</button>
}

セッションの取得

ユーザーがサインインした後、セッションが自動的に管理され、サーバーやクライアントサイドでセッション情報にアクセスできます。セッションの取得はサーバーサイドとクライアントサイドそれぞれの方法があります。

サーバーサイド

getServerSessionの引数に設定オブジェクトを渡すことでサーバーサイドでセッションを取得できます。

auth.ts
import { getServerSession } from "next-auth";

// NextAuth.jsの設定オブジェクト
export const authOptions: NextAuthOptions = {
~~
};

// ラップして引数不要で利用できるセッション取得関数を作成
export const getServerAuthSession = () => getServerSession(authOptions);

毎回設定オブジェクトをimportしたくないのでカスタムでラップ関数を作成してexportしています。

app/page.tsx
import { getServerAuthSession } from "@/server/auth";

export default async function Page() {
  const session = await getServerAuthSession();
  return <p>{session && <span>Logged in as {session.user?.name}</span>}</p>;
}
クライアントサイド

useSessionというクライアントサイドでセッションを取得するフックを使用してセッションを取得します。しかし、その前にセッションを取得するClient ComponentsをSessionProviderでラップする必要があります。

SessionProviderをServer Componentsで直接importして使うことができないためラップしたプロバイダーを独自で作成します。

context/WrapSessionProvider.tsx
"use client";

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

type WrapSessionProvider = {
  children: ReactNode;
};
export const WrapSessionProvider = ({ children }: WrapSessionProvider) => {
  return <SessionProvider>{children}</SessionProvider>;
};

Server Componentsで独自で作成したWrapSessionProviderをimportし、useSessionを使用するClient Componentsをラップします。

test-session/page.tsx
import { ClientSession } from "@/components/ClientSession";
import { WrapSessionProvider } from "@/context/WrapSessionProvider";

export default async function Home() {
  return (
    <div>
      セッション取得
      <WrapSessionProvider>
        <ClientSession /> // このClient Componentsの中でセッションを取得する
      </WrapSessionProvider>
    </div>
  );
}

useSessionを使用して、セッションを取得することができました。

components/ClientSession.tsx
"use client";
import { useSession } from "next-auth/react";
import React from "react";

export const ClientSession = () => {
  const { data: session, status } = useSession();
  console.log("session", session);
  console.log("status", status);
  return <div>クライアントサイドセッション取得</div>;
};

3.tRPCとの連携

tRPCとは

tRPCはTypeScriptを使用した型安全なAPIを構築するためのRPCフレームワークです。フロントエンド、バックエンド間で型を共有し、型安全で効率的に開発を進めることができます。

ここから先はt3-appのリポジトリをもとに解説していきます。
https://create.t3.gg/
リポジトリの解説は以前の記事で書いているので気になる方はご覧ください。
https://zenn.dev/kiwichan101kg/articles/279cc65988a39b

tRPCを使った認証におけるメリット

tRPCを使うことで、リクエストごとに認証が必要なプロシージャとそうでないプロシージャを簡単に切り分けることができます。これにより、認証処理を一元化し、コードの再利用性を高められます。
たとえば以下のようなプロシージャを作成し、各APIエンドポイントで使い分けることで簡単に認証有無を管理することができます。

  • protectedProcedure:認証済みのユーザーのみがアクセスできるルートを定義するためのプロシージャ
  • publicProcedure:認証なしでアクセスできるルートを定義するためのプロシージャ

コンテキストとは

APIで認証を行う際、セッションの有無を確認したい場合があります。その際に役立つのが「コンテキスト」です。コンテキストとは、セッション情報やデータベース接続など、各リクエストに共通する情報を提供する仕組みを指します。リクエストの処理中にコンテキストにアクセスし、セッション情報を見て、ユーザーが認証されているかどうかを確認することができます。

コンテキストを定義する

server/api/trpc.ts
import { getServerAuthSession } from "@/server/auth";

export const createTRPCContext = async (opts: { headers: Headers }) => {
  const session = await getServerAuthSession();

  return {
    db,
    session,
    ...opts,
  };
};

createTRPCContextは、リクエストごとにコンテキストを生成し、セッション情報やデータベース接続情報など、コンテキストとして扱いたい情報を設定して返します。
これにより、後続のリクエスト処理中にセッションにアクセスできるようになります。

tRPCの初期化、コンテキストを接続

tRPCを初期化し、<typeof createTRPCContext>のようにコンテキストの型情報を指定することで、リクエスト処理中にコンテキストにアクセス可能にします。

server/api/trpc.ts
const t = initTRPC.context<typeof createTRPCContext>().create();

認証プロシージャを作成

コンテキスト内のセッション情報を確認し、認証を行うプロシージャを作成します。

export const protectedProcedure = t.procedure
  .use(({ ctx, next }) => {
    if (!ctx.session || !ctx.session.user) {
      throw new TRPCError({ code: "UNAUTHORIZED" });
    }
    return next({
      ctx: {
        session: { ...ctx.session, user: ctx.session.user },
      },
    });
  });

コンテキストに定義した値はctx.sessionのようにアクセスすることができ、セッションの確認を行えます。

認証付きのAPIを定義

作成したprotectedProcedureを拡張することで認証付きのAPIを定義することができます。

server/api/routers/chat.ts
import {
  createTRPCRouter,
  protectedProcedure,
} from "@/server/api/trpc";

export const chatRouter = createTRPCRouter({
  create: protectedProcedure
    .input(z.object({ name: z.string().min(1) }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.post.create({
        data: {
          name: input.name,
          createdBy: { connect: { id: ctx.session.user.id } },
        },
      });
    }),
});

4.Prismaとの連携

T3 Stackでは、認証機能の実装にNextAuth.jsを、データベース操作にPrismaを使用しています。
NextAuth.jsではデータベースとの連携を効率的に行うために、Prismaアダプターが提供されています。

Prismaとは

PrismaとはTypescript環境で利用できるデータベースとのやり取りをよりシンプルにするためのORMです。開発者はSQLを直接書く代わりに、TypeScriptのコードでデータベース操作を記述することができます。T3 Stackでは、データベースにSQliteやMySQL、PostgreAQLなどが選択できます。
https://www.prisma.io/

Prismaアダプターの役割

PrismaアダプターはNextAuth.jsとPrismaを連携させ、認証に関連するデータをデータベースに保存・管理するための仕組みを提供します。これにより、セッション情報やユーザーデータなどを簡単にデータベースで扱えるようになります。

Prismaのスキーマ設定

NextAuth.jsが期待するデータ構造は決まっているため、それに沿ってPrismaのスキーマを設定する必要があります。以下の4つのモデルが必要です。

  • User: ユーザー情報を保存
  • Account: OAuth認証プロバイダーのアカウント情報を保存
  • Session: ユーザーセッション情報を保存
  • VerificationToken: メール認証などで使用するトークンを保存
schema.prismaファイル
schema.prisma
model Account {
    id                       String  @id @default(cuid())
    userId                   String
    type                     String
    provider                 String
    providerAccountId        String
    refresh_token            String? // @db.Text
    access_token             String? // @db.Text
    expires_at               Int?
    token_type               String?
    scope                    String?
    id_token                 String? // @db.Text
    session_state            String?
    user                     User    @relation(fields: [userId], references: [id], onDelete: Cascade)
    refresh_token_expires_in Int?

    @@unique([provider, providerAccountId])
}

model Session {
    id           String   @id @default(cuid())
    sessionToken String   @unique
    userId       String
    expires      DateTime
    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
    id            String    @id @default(cuid())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
    posts         Post[]
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@unique([identifier, token])
}

スキーマ定義してPrisma studioで確認すると User、Account、Session、VerificationTokenのテーブルが作成されていることが確認できます。

NextAuth.jsの設定

NextAuth.jsの設定ファイルで、Prismaアダプターを設定します。

src/server/auth.ts
import { PrismaAdapter } from "@auth/prisma-adapter";
import { type Adapter } from "next-auth/adapters";

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(db) as Adapter,
  callbacks: {
   ... 
  },
  providers: [
    ...
  ],
};

Prismaアダプターが設定されると、NextAuth.jsはセッション情報を自動的にPrismaを経由してデータベースに保存します。
これらの処理は全てPrismaアダプターによって自動的に行われるため、開発者は認証ロジックに集中することができます。

サインインするとデータベースにセッション情報が保存され、サインアウトするとセッション情報が削除されることが確認できました。

まとめ

今回は、NextAuth.js、tRPC、Prismaを組み合わせた効率的な認証システムついて解説しました。これらのツールを連携させることで、型安全性を保ちながら、効率的な認証システムを構築できます。

次回はT3 Stackでのエラーハンドリングについて投稿予定なので、ぜひご期待ください!

技術書典17に共同著書で出版することが決まりました!🎉

この度、技術書典17にて「T3 Stack」に関する書籍を共同著書で出版することが決まりました!🎉

この本では、各技術の基礎からT3 Stackとしての連携方法までを丁寧に解説し、体系的に学べる内容を目指しています。T3 Stackに興味がある方、これから学びたい方、ぜひフォローして最新情報をお待ちください!🙌
https://techbookfest.org/event/tbf17

興味がある方はこちらの記事もご覧ください!
https://zenn.dev/maicom/articles/efafe3fc3f40e2
https://zenn.dev/blueish/articles/4b2ae3781ade57
https://zenn.dev/kiwichan101kg/articles/b44305e3049bac

Discussion