🐆

Next.js × NextAuth × Prisma × VercelPostgresで構築するモダン認証機能システム

2023/06/06に公開6

はじめに

認証機能を一から作成したいと思い、Next.jsとNextAuthを使ったGithub認証機能の実装を行ったので、その手順を記事していきます。ユーザーデータ管理にPrismaを、データベースはVercelPostgresを使用しています。



ソースコード

実装したサンプルデータは下記リポジトリに格納しています。
https://github.com/MASAKi-cell/next-vecelPsotgres-app



バージョン情報

今回実装したバージョン情報のです。

  • next.js: v13.4.1
  • next-auth: v4.22.1
  • prisma/client: v4.14.1
  • vercel/postgres: v0.3.0
  • typescript: v5.0.4



技術詳細

Prisma

https://www.prisma.io/docs/concepts/components/prisma-schema

PrismaはNode.jsとTypeScriptによる、オープンソースORM(Object Relational Mapping)です。SQL(select, insert, update, delete...etc)の代わりにオブジェクトのメソッド(create, update, delete)を使用する為、SQLの操作に慣れていなくても、データベース操作を効率的に行うことが可能です。データベーススキーマがTypeScriptの型にマッピングされ、コンパイル時にデータベースクエリのエラーを検出できる為、型安全性が担保されます。


Vercel Postgres

https://vercel.com/storage/postgres

Vercel Postgresはその名前が示すとおりPostgreSQLベースのサーバレスデータベースです。2023年5月1日にVercelが発表した新機能のStorageサービスの一つです。


NextAuth.js

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

NextAuth.jsはNext.jsアプリケーションで認証機能を実装するためのJavaScriptライブラリです。OAuthをサポートしており、主要な認証プロバイダー(Google、Facebook、Twitter、GitHub)の連携も簡単に行うことが可能です。自前で認証機能を実装することに比べて、コスト(運用やセキュリティ面のリスク)を下げることができます。



Vecel CLIの導入

https://vercel.com/docs/cli

まず初めに、vercel CLIをインストールします。
Vercel CLIを使用することで、本番環境のデプロイや環境変数を追加、一覧表示、削除などをターミナル上で効率的に行うことができます。

npm i -g vercel


以下のコマンドを実行してバージョンが表示されていれば、インストール成功です。

vercel --version
Vercel CLI 29.3.3
29.3.3



Vecel Postgresのセットアップ

Vecel Postgresのセットアップを行います。
VecelダッシュボードにアクセスしてCreat Databaseをクリックして、サーバレスデータベースとしてPostgresを選択します。




database名とRegionを指定してデータベースを作成します。
現在、リージョンの選択肢に日本がないため、シンガポールを選択します。




以下の画面が表示されていればセットアップ完了です。



ローカルプロジェクトとVecel Postgresを接続する

ローカルプロジェクトとVecel Postgresを接続するために、vercel linkを使用します。

https://vercel.com/docs/cli/link
vercel linkはローカルのプロジェクトディレクトリをVercelのプロジェクトに関連付けるためコマンドです。設定をすることでvercel devvercel deployなどのコマンドをローカル上で使用することができ、Vercelにデプロイすることが可能になります。


vercel linkの設定が全て完了するとローカルのプロジェクトに.vercelフォルダが作成されます。

vercel link
Vercel CLI 29.3.3
> > No existing credentials found. Please log in:
? Log in to Vercel 
● Continue with GitHub 
○ Continue with GitLab 
○ Continue with Bitbucket 
○ Continue with Email 
○ Continue with SAML Single Sign-On 
  ─────────────────────────────────
○ Cancel 

? Set up “~/Desktop/next-app”? [Y/n] y
? Which scope should contain your project? masaki-cell
? Link to existing project? [y/N] n
? What’s your project’s name? next-auth-app
? In which directory is your code located? 
? Want to modify these settings? [y/N] y
? Which settings would you like to overwrite (select multiple)? None
✅  Linked to masaki-cell/next-auth-app 


Vecelダッシュボードに戻り、connecl Projectを選択して、Vercel Postgresとローカルプロジェクトを接続します。


Vercel Postgresとローカルプロジェクトが接続できたら、vercel env pullコマンドを使用してデータベースのcredentail情報をローカルに取り込む作業を行います。実行後、POSTGRES_PASSWORDやPOSTGRES_PRISMA_URLなどvercelの環境変数が.env取り込まれます。

vercel env pull .env

+ POSTGRES_PRISMA_URL (Updated)
+ NX_DAEMON
+ POSTGRES_DATABASE
+ POSTGRES_HOST
+ POSTGRES_PASSWORD
+ POSTGRES_URL
+ POSTGRES_URL_NON_POOLING
+ POSTGRES_USER
+ TURBO_REMOTE_ONLY
...



Prismaのセットアップ

次にprismaの設定を行います。
まずは必要なパッケージをインストールします。

npm install @vercel/postgres
npm install prisma -D


インストール後、上記コマンドを実行して、primsaを初期化します。初期化することでprisma/schema.prismaが作成され、primsaに接続するDBの詳細やモデルを指定することが可能となります。

npx prisma init


schema.prismaにデータベースへの接続情報を設定します。

schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url = env("POSTGRES_PRISMA_URL") // uses connection pooling
  directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
  shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING") // used for migrations
}
  • provider: データベースプロバイダとしてPostgreSQLを使用することを指定しています。
  • url: データベース接続用のURLです。
  • directUrl: 直接データベースを接続する際に使用するURLです。
  • shadowDatabaseUrl: マイグレーション用のシャドウデータベースのURLです。本番データベースに影響を与えることなく、マイグレーションを実行できます。



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)

  @@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[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}
  • @default(cuid()) とすることで、cuid 仕様に基づき、グローバルに一意な識別子が生成されます。
  • @@unique([provider, providerAccountId])はproviderとproviderAccountIdの組み合わせがデータベース全体で一意であることを強制する一意性制約であることを示します。その為、あるAccountのproviderとproviderAccountIdの組み合わせと同じ組み合わせを持つ別のAccountを作成することはできません。
  • @relationはuserモデルとAccountモデルがどのような関係にあるのかを示しています。references: [id]がAccountのuserIdフィールドの値は、関連するUserのidフィールドの値と一致し、onDelete: CascadeはUserのインスタンスが削除されたときに、それに関連するAccountのインスタンスも自動的に削除されること意味しています。

データベースと連携してユーザー情報を管理する場合は、NextAuth.jsが期待するテーブル構造にする必要があります。Accountモデルがユーザが認証プロバイダ(例えば、GoogleやFacebookなど)を通じてアカウントを作成したときの情報を示し、Userモデルがユーザ名、メールアドレス、画像などの基本的なユーザ情報が含まれます。VerificationTokenはメールアドレスを使ったパスワードレスログインが必要な場合に、Sessionはセッションをデータベースで管理する場合に使用します。

https://authjs.dev/reference/adapters#models


ちなみにデータベースを使用せずに、ユーザーを管理する場合は、セッションの保存先にJWTなどを指定する必要があります。
https://zenn.dev/nrikiji/articles/d37393da5ae9bc



マイグレーションファイルの作成

次にイグレーションファイルの生成、生成されたマイグレーションの適用、Prisma Clientの生成(または更新)を実行します。

npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "verceldb", schema "public" at "ep-plain-mud-160309.ap-southeast-1.postgres.vercel-storage.com"

Applying migration `20230522011708_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20230522011708_init/
    └─ migration.sql

Your database is now in sync with your schema.

マイグレーションは、データベースのスキーマ(テーブル構造やインデックス)を変更することが可能です。テーブルの追加や既存のテーブルにカラムを追加するなどの変更が生じた場合、スキーマーの変更を手動で行うこともできますが、ミスが生じる可能性が高く、マイグレーションを通じて、変更履歴の追跡や変更の削除を行うことができます。開発者は安全に、かつ一貫性を保った形でデータベースのスキーマを変更することが可能になります。


最後にprisma studioを立ち上げて、ユーザー情報が格納されていることを確認できたら、prismaのセットアップは完了です。

npx prisma studio



PrismaClientを使用したデータベースクライアントの生成

データベースにクエリを送るためにPrismaClientを生成します。

import { PrismaClient } from "@prisma/client";

let prisma: PrismaClient;

const globalForPrisma = global as unknown as {
  prisma: PrismaClient | undefined;
};

if (!globalForPrisma.prisma) {
  globalForPrisma.prisma = new PrismaClient();
}
prisma = globalForPrisma.prisma;

export default prisma;

データベース接続をリロードする際に、余分なPrismaClientインスタンスが生成された場合、それぞれのPrismaClientインスタンスが独自の接続プール(データベース接続のキャッシュ)を保持してしまう為、グローバルオブジェクトにPrismaClientインスタンスを保存しておき、余分なインスタンスが生成されないように実装します。

https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices



GitHub OAuthの設定

GitHubダッシュボード サイドバーの「Developer settings」に遷移して、OAuth AppsをクリックしてOAuth Appsの設定を行います。


Homepage URLに http://localhost:3000、Authorization callback URLにhttp://localhost:3000/api/auth/callback/githubを設定します。Application nameは任意の名前で問題ありません。

最後に、「Register application」をクリックすると、Client ID と Client secretが生成され、それらを使用して、Githubと接続していきます。



nextAuth.jsの実装

次に、nextAuth.jsの実装を行います。
まずは、必要なパッケージをインストールします。

npm i next-auth @next-auth/prisma-adapter

.envに先ほど生成した、GitHub OAuthのClient IDとClient secretを設定します。NEXTAUTH_URLは'http://localhost:3000を指定します。SECRETはトークンのハッシュ、Cookie の署名/暗号化、暗号キーに使用されます。

.env
GITHUB_ID="生成したGithubID"
GITHUB_SECRET="生成したGithub Secret"
NEXTAUTH_URL="http://localhost:3000"
SECRET="openssl randで生成されたランダムなデータ"


openssl rand -base64 32

openssl rand -base64 32コマンドは、OpenSSLを使用して32バイトのランダムなデータを生成し、それをBase64エンコードするもので、文字列として出力されます。パスワード、APIキー、暗号化キーなどセキュリティ関連のキーを設定する際に使用します。

https://next-auth.js.org/configuration/options#nextauth_secret


pages/api/authフォルダ配下に[...nextauth].tsを作成して、nextAuthの設定を行います。

api/auth/[...nextauth].ts
import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import GitHubProvider from "next-auth/providers/github";
import prisma from "lib/prisma";

const authHandler: NextApiHandler = (req, res) =>
  NextAuth(req, res, authOptions);

/**
 * Configure NextAuth
 */
const authOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID ?? "",
      clientSecret: process.env.GITHUB_SECRET ?? "",
    }),
  ],
  adapter: PrismaAdapter(prisma),
  secret: process.env.SECRET,
  pages: {
    signIn: "auth/login",
  },
};

export default authHandler;
  • providersに認証方法を指定(今回はGithub)します。
  • secretはopenssl コマンドで生成したランダムデータを配置して秘密鍵を生成します。
  • pagesでログイン画面の設定を行います。



プロバイダーの追加

今回はGithubを指定していますが、TwitterやGoogleなど別のプロバイダーで認証したい場合は、providersを増やしていくことで、認証機能を追加できます。

api/auth/[...nextauth].js
import TwitterProvider from "next-auth/providers/twitter"
...
providers: [
  TwitterProvider({
    clientId: process.env.TWITTER_ID,
    clientSecret: process.env.TWITTER_SECRET
  })
],
...



ログイン画面の実装

login.tsxに、ログイン画面を作成していきます。

auth/login.tsx
import { getProviders, signIn } from "next-auth/react";
import styles from "style/styles.module.css";
import Image from "next/image";

/** types */
import { InferGetServerSidePropsType } from "next";

const login = ({
  providers,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  return (
    <div className={styles.BackgroundPaper}>
      <div>
        <Image
          src="/github-mark.svg"
          width={150}
          height={150}
          objectFit="contain"
          alt={"Github Logo"}
        />
        {providers &&
          Object.values(providers).map((provider) => {
            return (
              <div key={provider.name}>
                <button
                  className={styles.githubButton}
                  onClick={() =>
                    signIn(provider.id, {
                      callbackUrl: "/top/main",
                    })
                  }
                >
                  <span className="">Sign in with {provider.name}</span>
                </button>
              </div>
            );
          })}
      </div>
    </div>
  );
};

export default login;

/**
 * プロバイダーリストを取得
 */
export const getServerSideProps = async () => {
  const providers = await getProviders().then((res) => {
    console.log(res, "<<<<< : provider response");
    return res;
  });

  return {
    props: { providers },
  };
};

getProviders()メソッドを使用して、プロバイダー情報を取得し、ログイン画面に表示させます。プロバイダーが複数存在する場合は、複数表示されます。

このままnpm run devでプロジェクトを立ち上げてhttp://localhost:3000/auth/loginにアクセスすると、Githubのログイン画面が表示されるようになります。


postinstallの設定

ログイン画面の実装は完了ですが、ログイン画面にて、ログインボタンをクリックしても以下のようなエラーが表示されて、ログインすることができません。

- error Error: Prisma has detected that this project was built on Vercel, 
which caches dependencies. This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered. 
To fix this, make sure to run the `prisma generate` command during the build process.

Learn how: https://pris.ly/d/vercel-build

VercelとPrimaを連携させる場合、Vercel cachesに依存するためエラーが発生します。
Prismaは、依存関係がインストールされるときにpostinstallを使用して Prisma クライアントを生成しますが、 Vercelはキャッシュされたモジュールを使用するため、postinstallは、最初のデプロイメント後の後続のデプロイメントでは実行されないようです。

This issue can be solved by explicitly generating Prisma Client on every deployment. Running prisma generate before each deployment will ensure Prisma Client is up-to-date.
https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/vercel-caching-issue


その為、事前にpackage.jsonにスクリプトを登録しておき、postinstallを立ち上げてから、npm run devを実行します。

package.json
{
  ...
  "scripts" {
    "postinstall": "prisma generate"
  }
  ...
}
npm run postinstall

> next-app@0.1.0 postinstall
> prisma generate

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (4.14.1 | library) to ./node_modules/@prisma/client in 63ms
You can now start using Prisma Client in your code.


postinstallを立ち上げた状態から、ログインボタンを実行すると、Githubと正常に連携することができます。


npx prisma studio

prisma studioにGithubアカウントが追加されていれば成功です。


ログアウト機能の追加

最後にログアウト機能を追加しておきます。

top/main.tsx
import { signOut } from "next-auth/react";
・
・
・
<a
 className={styles.link}
  onClick={() =>
  signOut({
   callbackUrl: "/auth/login",
   })
  }
 >
 Sign out
</a>



Vercelにデプロイ

最後にVercelにローカルプロジェクトをデプロイします。デプロイの方法は以前書いた記事に掲載しています。
https://zenn.dev/arsaga/articles/bfe2ff5b6ee7a9



さいごに

Vercel PostgresやPrismaなど初めて触りましたが、実装することができました。もし何か間違いがあれば、ご指摘いただけると幸いです。ここまで読んでくださりありがとうございました。



参考文献

https://qiita.com/am_765/items/5e42bd5f87b296f61fbc

https://reffect.co.jp/node-js/prisma-basic

https://zenn.dev/tsucchiiinoko/articles/f222dbbfa23325

https://engineering.nifty.co.jp/blog/9817

Arsaga Developers Blog

Discussion

tak458tak458

認証機能ではなく認可機能なのでは?細かい部分は読んでいませんがGitHubが認証機能を担っているイメージでした。

MSKMSK

横から失礼します。

Q. 認証機能ではなく認可機能なのでは?

A.こちらのプロジェクトでは、ユーザーにログイン機能を提供するのが目的なので、認証機能になると思います。

認証 or 認可の違いについて

  • 認証: 誰なのかを明示する
  • 認可: 権限があるのかの確認
tak458tak458

失礼しました。訂正ありがとうございます。
改めて見直したところ、認証機能で良いと思います。重ね重ね失礼しました。

MSKMSK

何か色々ややこしいですよね(´・ω・`)

mash180sxmash180sx

基本的な質問ですみません。
Next 13を利用されているということですが、App Router を利用されて実装されているのでしょうか。