Next.jsのMiddlewareとAuth.js(NextAuth)の効率的な活用方法 Edgeランタイム対応の認証設計
はじめに
Next.jsアプリケーションに認証機能を実装する際、ミドルウェアを使った効率的な設計がポイントになると思いますが、特にNext-Authを使う場合、Edgeランタイムの制限を理解し適切な実装方法を選ぶ必要があるということを最近学びました。
自身の学びから本記事ではミドルウェアでのPrisma利用制限に焦点を当て効率的な認証設計パターンを紹介します。
問題点: ミドルウェアでのPrisma利用
Next.jsのミドルウェアはEdgeランタイム上で動作するため、Prismaのような特定のNode.js依存ライブラリを直接使用できません。
つまり、以下のような状態になるとうまく動きません。
// 以下のようなコードはミドルウェアでは動作しない
export async function middleware(request) {
// ❌ EdgeランタイムではPrismaが動作しない為
const user = await prisma.user.findFirst({
where: { id: userId }
});
}
middleware内でいきなりprismaを呼び出すことは少ないかもしれません。
もう少し具体的な例を示すと以下のようなauth.tsを設定いるとき、これを直接middlware.tsで呼び出すことができないということになります。
import { PrismaAdapter } from '@auth/prisma-adapter';
import { compareSync } from 'bcrypt-ts-edge';
import NextAuth, { NextAuthConfig } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '@/db/prisma';
export const config = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
async authorize(credentials) {
// ❌ EdgeランタイムではPrismaが動作しない為
const user = await prisma.user.findFirst({...});
// ...
},
}),
],
// その他の設定
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(config);
export { auth as middleware } from '@/auth'
解決策: 二層構造の認証設計
この問題を解決するために、Next-Authの設定を2つに分けるパターンが効果的です:
-
Edgeランタイム用の軽量設定 (
auth.config.ts
) -
フル機能用の設定 (
auth.ts
)
実装例
1. ミドルウェア (Edge対応)
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const { auth: middleware } = NextAuth(authConfig);
import { NextResponse } from 'next/server';
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
providers: [],
callbacks: {
authorized({ request }) {
// Edge対応の軽量な処理
// 今回はcartのsessionIdをcookieで処理したかったのでこのような内容になります。
if (!request.cookies.get('sessionCartId')) {
const sessionCartId = crypto.randomUUID();
const response = NextResponse.next({
request: { headers: new Headers(request.headers) },
});
response.cookies.set('sessionCartId', sessionCartId);
return response;
}
return true;
},
},
} satisfies NextAuthConfig;
2. フル機能の認証設定 (Prisma対応)
import { PrismaAdapter } from '@auth/prisma-adapter';
import { compareSync } from 'bcrypt-ts-edge';
import NextAuth, { NextAuthConfig } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '@/db/prisma';
export const config = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
async authorize(credentials) {
// ここでPrismaを使ったDB操作が可能
const user = await prisma.user.findFirst({...});
// ...
},
}),
],
// その他の設定
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(config);
3. ページでの使用例
import { auth } from '@/auth'; // フル機能版のauthをインポート
const SignUpPage = async ({ searchParams }: SignInPageProps) => {
const { callbackUrl } = await searchParams;
const session = await auth(); // ここでフル機能のauthを使用
if (session) {
return redirect(callbackUrl || '/');
}
return (
// ページコンテンツ
);
};
この設計パターンのメリット
-
パフォーマンス最適化
- ミドルウェアは高速なEdgeランタイムで処理
- 重いDB操作はサーバーサイドに分離
-
エッジケースへの対応
- Edgeランタイムの制限を回避
- Node.js固有の機能を必要とするコードを適切に分離
-
セキュリティとUXの両立
- 全ルートに対する「Zero Trust」アプローチ
- 重いDB操作を分離してページロード速度を維持
-
コードの明確な責務分離
- ミドルウェア: ルートレベルの認証・保護
- auth関数: ユーザーデータの詳細取得
実装時の注意点
-
環境変数の管理
- EdgeランタイムとNode.jsランタイムで利用できる環境変数が異なる場合がある
-
型の一貫性
- 両方の認証オブジェクトで一貫した型定義を維持する
-
認証状態の同期
- ミドルウェアとAPIルートで認証状態が一致するよう注意する
まとめ
Next.jsのミドルウェアとNext-Authを組み合わせる際は、Edgeランタイムの制限を理解し、適切な設計パターンを選択することが重要です。二層構造の認証設計を採用することで、パフォーマンスとセキュリティを両立させたアプリケーションを構築できます。
Prismaなどのデータベースアクセスは直接ミドルウェアでは行わず、適切にフル機能の認証層に分離することで、多くの問題を回避できます。この設計パターンを理解し、プロジェクトに適用することで、より堅牢で高速なWeb認証システムを実現できるかと思います。皆様のプロジェクトでも参考になれば幸いです。
余談
Discussion