Next.js × NextAuth × Prisma × VercelPostgresで構築するモダン認証機能システム
はじめに
認証機能を一から作成したいと思い、Next.jsとNextAuthを使ったGithub認証機能の実装を行ったので、その手順を記事していきます。ユーザーデータ管理にPrismaを、データベースはVercelPostgresを使用しています。
ソースコード
実装したサンプルデータは下記リポジトリに格納しています。
バージョン情報
今回実装したバージョン情報のです。
- 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
PrismaはNode.jsとTypeScriptによる、オープンソースORM(Object Relational Mapping)です。SQL(select, insert, update, delete...etc)の代わりにオブジェクトのメソッド(create, update, delete)を使用する為、SQLの操作に慣れていなくても、データベース操作を効率的に行うことが可能です。データベーススキーマがTypeScriptの型にマッピングされ、コンパイル時にデータベースクエリのエラーを検出できる為、型安全性が担保されます。
Vercel Postgres
Vercel Postgresはその名前が示すとおりPostgreSQLベースのサーバレスデータベースです。2023年5月1日にVercelが発表した新機能のStorageサービスの一つです。
NextAuth.js
NextAuth.jsはNext.jsアプリケーションで認証機能を実装するためのJavaScriptライブラリです。OAuthをサポートしており、主要な認証プロバイダー(Google、Facebook、Twitter、GitHub)の連携も簡単に行うことが可能です。自前で認証機能を実装することに比べて、コスト(運用やセキュリティ面のリスク)を下げることができます。
Vecel 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
を使用します。
vercel link
はローカルのプロジェクトディレクトリをVercelのプロジェクトに関連付けるためコマンドです。設定をすることでvercel dev
やvercel 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
にデータベースへの接続情報を設定します。
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
に認証のためのモデルを指定します。
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はセッションをデータベースで管理する場合に使用します。
ちなみにデータベースを使用せずに、ユーザーを管理する場合は、セッションの保存先にJWTなどを指定する必要があります。
マイグレーションファイルの作成
次にイグレーションファイルの生成、生成されたマイグレーションの適用、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インスタンスを保存しておき、余分なインスタンスが生成されないように実装します。
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 の署名/暗号化、暗号キーに使用されます。
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キー、暗号化キーなどセキュリティ関連のキーを設定する際に使用します。
pages/api/authフォルダ配下に[...nextauth].ts
を作成して、nextAuthの設定を行います。
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を増やしていくことで、認証機能を追加できます。
import TwitterProvider from "next-auth/providers/twitter"
...
providers: [
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
})
],
...
ログイン画面の実装
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
を実行します。
{
...
"scripts" {
"postinstall": "prisma generate"
}
...
}
npm run postinstall
> [email protected] 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アカウントが追加されていれば成功です。
ログアウト機能の追加
最後にログアウト機能を追加しておきます。
import { signOut } from "next-auth/react";
・
・
・
<a
className={styles.link}
onClick={() =>
signOut({
callbackUrl: "/auth/login",
})
}
>
Sign out
</a>
Vercelにデプロイ
最後にVercelにローカルプロジェクトをデプロイします。デプロイの方法は以前書いた記事に掲載しています。
さいごに
Vercel PostgresやPrismaなど初めて触りましたが、実装することができました。もし何か間違いがあれば、ご指摘いただけると幸いです。ここまで読んでくださりありがとうございました。
参考文献
アルサーガパートナーズ株式会社のエンジニアによるテックブログです。11月14(木)20時〜 エンジニア向けセミナーを開催!詳細とご応募は👉️ arsaga.jp/news/pressrelease-cheer-up-project-november-20241114/
Discussion
認証機能ではなく認可機能なのでは?細かい部分は読んでいませんがGitHubが認証機能を担っているイメージでした。
横から失礼します。
A.こちらのプロジェクトでは、ユーザーにログイン機能を提供するのが目的なので、認証機能になると思います。
認証 or 認可の違いについて
失礼しました。訂正ありがとうございます。
改めて見直したところ、認証機能で良いと思います。重ね重ね失礼しました。
何か色々ややこしいですよね(´・ω・`)
基本的な質問ですみません。
Next 13を利用されているということですが、App Router を利用されて実装されているのでしょうか。
App Routerは今回使用しませんでした