エッジランタイムがますます普及するにつれて、Auth.jsとnext-auth
をこれらの環境にデプロイしようとする人が増え、現在、エコシステム全体に影響を与える根本的な互換性の問題に直面しています。このドキュメントでは、理解度や経験に関わらず、皆様が直面する課題を理解し、選択したランタイムでAuth.jsを稼働させるお手伝いができればと思っています!
まず、いくつかの背景知識を説明します。これらの内容に精通している場合は、このセクションをスキップしてください!
定義
ここでは、Auth.jsと、現在さまざまなフレームワーク、ホスティングプロバイダー、ライブラリなどで非常に人気のあるエッジランタイムとの関連について具体的に説明します。
まず、このコンテキストにおける「エッジ」とは何かを明確にしましょう。「エッジ」とは、ネットワークエンジニアリングの用語を借用したもので、ネットワークのエッジ、つまりユーザーに近い場所に配置された計算ノード(つまりサーバー)を指します。通常、これらは、ほとんどの重要なワークロードを実行するデータセンターの中核で見られる本格的なサーバーよりも低電力な計算ノードです。ここでコードを実行することの利点としては、ユーザーのエンドデバイスへのレイテンシの低減、スケーラビリティの向上、およびよりコスト効率の高いコンピューティングなどが挙げられます。欠点としては、ハードウェアの性能が低いことや、ソフトウェアスタックの互換性が異なる可能性があることが挙げられます。
したがって、「エッジランタイム」と言う場合、Node.jsではないサーバーサイドJavaScriptランタイムであり、これらのエッジコンピューティングノード(サーバー)での実行に最適化されていることを意味します。一般的に、これはコードがユーザーにより近い低電力ハードウェア上で実行され、高速な起動時間、低メモリ使用量などに最適化されていることを意味します。
これは問題です。これらのランタイムでは、Node.jsにある機能が欠けていることが多く、場合によっては、依存するライブラリやパッケージの機能に不可欠なものもあります。パッケージが「エッジ対応」または「エッジ対応済み」であると主張する場合、実際には、いくつかのエッジランタイムで欠落しているNode.jsの機能/モジュールを回避するためにソフトウェアを設計したことを意味します。これにより、より普遍的な互換性が実現します。unjsの互換性マトリックスを確認して、どのランタイムがどの機能をサポートしているかを確認してください。Auth.jsには不可欠ではありませんが、JavaScriptランタイムがAPI相互運用性について連携するためのスペースを提供する業界グループであるWinterCGについてもここで言及しておきましょう。
ここで、これらの機能/モジュールは、実行されている基盤となる環境がそれらを提供していないために欠落していることが多いことに注意しておきます。たとえば、開発者はいくらでも時間を投資できますが、サーバーサイドJavaScriptランタイムが、ファイルシステムへのアクセス権を与えないサンドボックス化されたオペレーティングシステム環境で実行される場合、どれだけ努力してもfs
モジュールを実装することはできません。
このNode.js対その他のランタイムの状況は、現時点では非常に断片的で流動的であるため、多くのライブラリは、fetch
など、最も共通の機能のみを使用するようにワークロードを最適化しています。たとえば、データベースプロバイダーの場合、クライアントライブラリがバックエンドと通信するためにHTTPリクエストを行うだけで済むようにシステムを設計できる場合は、「エッジ対応」としてライブラリを宣伝し、ユーザーが希望する場所で実行できます。これは、たとえば、バックエンドと通信するためにNode.jsから生のTCPソケットを使用しなければならない他のデータベースクライアントライブラリとは対照的です。
Auth.js
エッジ互換性は、Auth.jsが最適化している点です。つまり、選択した任意のJavaScriptランタイムでAuth.jsの中核機能を実行できます。ただし、重要なのは「中核機能」です。Auth.js / next-auth
のみを使用し、Auth.jsのコールバック、ミドルウェアなどで他のライブラリを使用しない場合、どこにでも使用できます!
問題は、Auth.jsと他のライブラリを使用したい場合に発生します。
問題点
データベースアダプター
包括的な認証システムを実装するためにAuth.jsと組み合わせる一般的なパッケージは、データベースクライアントです。データベースクライアントは、データベースサーバーと直接通信するためにTCPソケットを利用することが多いため問題があります。PostgreSQLはそのような一般的なデータベースの1つです。
PostgreSQLは、クライアントとサーバー間の通信にメッセージベースのプロトコルを使用するデータベースであり、TCP(またはUnix)ソケットを介して転送されます。生のTCPソケットは、一般にエッジランタイムでは使用できないNode.jsの機能の1つです。したがって、表面上は、エッジランタイムで実行されているJavaScriptからPostgreSQLデータベースと通信することは不可能のように見えます。これは、他の多くのデータベースとそのそれぞれの通信プロトコルにも当てはまります。
しかし、エッジランタイムが成熟し、人気が高まるにつれて、人々は創造性を発揮し、この問題に対するさまざまな解決策を実装してきました。そのような一般的な解決策の1つは、データベースの前に何らかのAPIサーバーを配置することで、その目的はHTTP経由で送信されたデータベースクエリをデータベースが理解できるプロトコルに変換することです。これにより、クライアント側は、すべてのエッジランタイムがサポートするHTTPリクエストをAPIサーバーに行うだけで済みます。
ミドルウェア
Next.jsとnext-auth
では、Next.jsのミドルウェアを使用して、セッションが存在するかどうかを確認し、次にルーティングする場所を決定することでルートを保護することもできます。デフォルトでは、Vercelやその他のホスティングプロバイダーでは、ミドルウェアコードは常にエッジランタイムで実行されます。これは、たとえば、基盤となる機能が使用できない環境(つまりTCPソケット)でPostgreSQLクエリを実行しようとしていることを意味します。したがって、明示的に「エッジ対応」ではないデータベースアダプターを使用するには、使用可能な機能を使用してデータベースをクエリする方法を見つける必要があります。
解決策
データベースセッション戦略とデータベースアダプターを使用してAuth.jsを使用すると、通常の操作中にデータベースへの多くの呼び出しが行われます。使用しているフレームワークに関係なく、すべてのAuth.jsクライアントは現在アクティブなセッションを取得でき、これはデータベースをクエリして、ユーザーのsessionToken
がデータベースに存在し、有効(つまり期限切れではない)かどうかを確認することによって行われます。
これは、ユーザーの認証状態を確認したいアプリケーション内のあらゆる場所で、データベースへの呼び出しが必要になることを意味します。実際には、Auth.js はこれよりも少し賢く、キャッシングやその他の工夫を使用して不要なデータベースリクエストを回避しますが、すべてのauth()
呼び出しがデータベースクエリをトリガーすることを想像できます。そのため、エッジランタイムで多くのデータベースアダプターを使用するために、Auth.js を使用する何らかの回避策が必要です!
構成の分割
Next.js とnext-auth
を念頭に置いて、Auth.js をエッジランタイムで一部のコードを実行させながら、データベースを使用してセッションを保存するにはどうすればよいかを考えてみましょう。エッジ環境にはデータベース設定のないnext-auth
の別個の「バージョン」、その他すべてにはデータベースを含む別のバージョンが必要です。これを実現するには、Auth.js の“遅延初期化”機能を使用して、ミドルウェアにはアダプターを含まないスタンドアロンクライアントを、その他すべてには別のクライアントをインスタンス化します。
- まず、どこにでも使用できる共通の Auth.js 構成オブジェクトを作成します。これは、データベースアダプターを**含みません**。
import GitHub from "next-auth/providers/github"
import type { NextAuthConfig } from "next-auth"
// Notice this is only an object, not a full Auth.js instance
export default {
providers: [GitHub],
} satisfies NextAuthConfig
- 次に、その構成をインポートするだけでなく、アダプターを追加し、セッション戦略に
jwt
を使用する、別個に**インスタンス化された** Auth.js インスタンスを作成します。
import NextAuth from "next-auth"
import authConfig from "./auth.config"
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@auth/prisma-adapter"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
...authConfig,
})
- ミドルウェアは、データベースアダプターを含まない構成をインポートし、独自の Auth.js クライアントをインスタンス化します。
import NextAuth from "next-auth"
import authConfig from "./auth.config"
export const { auth: middleware } = NextAuth(authConfig)
- 最後に、他のすべての場所で、プライマリの
auth.ts
構成からインポートし、通常どおりnext-auth
を使用できます。詳細な例については、セッション管理に関するドキュメントを参照してください。
import { auth } from "@/auth"
export default async function Page() {
const session = await auth()
if (!session) {
return <div>Not authenticated</div>
}
return (
<div className="container">
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
)
}
ここで重要なのは、ミドルウェア内のnext-auth
からデータベース機能とサポートを削除したことです。つまり、ミドルウェアでコードを実行している間は、セッションやユーザーアカウントの詳細などの情報を取得できません。これは、/app/protected/page.tsx
ファイルで上記のように示したチェックに依存して、ルートを効果的に保護する必要があることを意味します。ミドルウェアは、たとえば、セッションクッキーの有効期限を延長するために使用されます。