mcpField
N
nextauth-authentication

nextauth-authentication

github.com/mindrallyセキュリティ2871.1k 回インストール3 週間前に更新
リポジトリ

nextauth-authentication について

Next.jsでNextAuth.js(Auth.js v5)による認証を実装するためのガイドです。セッション方式、OAuthと認証情報プロバイダー、アクセス制御、サーバー側でのセッション検証を扱います。

主な機能

  1. 01Auth.js v5の設定
  2. 02JWTとデータベースセッション
  3. 03OAuthと認証情報プロバイダー
  4. 04サーバー側のルート保護
  5. 05ロールベースのアクセス制御
  6. 06セキュリティとテストの指針

活用シーン

  1. 01Next.jsアプリへのサインイン追加
  2. 02APIルートとサーバーアクションの保護
  3. 03セッション方式の選択
  4. 04ユーザーロールによるページ制限

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mindrally/skills --skill nextauth-authentication

インストール後、Claude Code で/nextauth-authenticationを入力して読み込まれたか確認します。

詳細

NextAuth 認証

NextAuth.js(Auth.js v5)の認証実装に精通しています。Next.js アプリケーションに認証を統合する際は、以下のガイドラインに従ってください。

基本原則

  • Auth.js v5 のパターンと、ユニバーサルな auth() 関数を使用します
  • ニーズに応じた適切なセッション管理戦略を実装します
  • 機密性の高い操作では、必ずサーバー側でセッションを検証します
  • AUTH_ プレフィックスを付けて環境変数を正しく設定します

インストール

npm install next-auth@beta

環境変数

# Required
AUTH_SECRET=your-32-byte-secret-here  # Generate with: openssl rand -base64 32

# Provider credentials (auto-detected if named correctly)
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret

AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

# Optional: Custom URL (auto-detected in most environments)
AUTH_URL=https://your-domain.com

基本設定

auth.ts(ルート設定)
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub, // Credentials auto-detected from AUTH_GITHUB_ID/SECRET
    Google,
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        // Validate credentials
        const user = await validateCredentials(credentials);
        if (!user) return null;
        return user;
      },
    }),
  ],
  session: {
    strategy: 'jwt', // or 'database'
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/error',
  },
  callbacks: {
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }
      return session;
    },
    async authorized({ auth, request }) {
      const isAuthenticated = !!auth?.user;
      const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');

      if (isProtectedRoute && !isAuthenticated) {
        return false; // Redirect to sign-in
      }

      return true;
    },
  },
});
ルートハンドラー(app/api/auth/[...nextauth]/route.ts)
import { handlers } from '@/auth';

export const { GET, POST } = handlers;
ミドルウェア(middleware.ts)
import { auth } from '@/auth';

export default auth((req) => {
  const isAuthenticated = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith('/auth');
  const isProtectedRoute = req.nextUrl.pathname.startsWith('/dashboard');

  // Redirect authenticated users away from auth pages
  if (isAuthenticated && isAuthPage) {
    return Response.redirect(new URL('/dashboard', req.nextUrl));
  }

  // Redirect unauthenticated users from protected routes
  if (!isAuthenticated && isProtectedRoute) {
    return Response.redirect(new URL('/auth/signin', req.nextUrl));
  }
});

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

セッション戦略

JWT 戦略(アダプター未使用時のデフォルト)

最適な用途:サーバーレス、Edge ランタイム、データベースクエリの最小化

export const { auth } = NextAuth({
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
});

特徴:

  • セッションは暗号化された Cookie に保存されます
  • リクエストごとのデータベースクエリはありません
  • 有効期限が切れる前に無効化することはできません
  • Edge ミドルウェアで動作します
データベース戦略

最適な用途:セッションの即時無効化、「すべての場所からサインアウト」

export const { auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: 'database',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
});

特徴:

  • セッションはデータベースに保存されます
  • リクエストごとにデータベースクエリが発生します
  • 即時に無効化できます
  • Edge ミドルウェアとは互換性がありません(分割設定を使用します)
Edge とデータベースの分割設定
// auth.config.ts - Edge-compatible config
import type { NextAuthConfig } from 'next-auth';

export const authConfig: NextAuthConfig = {
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    authorized({ auth, request }) {
      return !!auth?.user;
    },
  },
  providers: [], // Configured in auth.ts
};

// auth.ts - Full config with adapter
import NextAuth from 'next-auth';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { authConfig } from './auth.config';

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  adapter: PrismaAdapter(prisma),
  providers: [GitHub, Google],
});

// middleware.ts - Uses edge-compatible config
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';

export default NextAuth(authConfig).auth;

コンポーネントでの認証

サーバーコンポーネント
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user) {
    redirect('/auth/signin');
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  );
}
クライアントコンポーネント
'use client';

import { useSession } from 'next-auth/react';

export function UserProfile() {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return <Skeleton />;
  }

  if (status === 'unauthenticated') {
    return <SignInPrompt />;
  }

  return (
    <div>
      <img src={session.user.image} alt={session.user.name} />
      <p>{session.user.name}</p>
    </div>
  );
}
セッションプロバイダーの設定
// app/providers.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

サーバーアクション

'use server';

import { auth, signIn, signOut } from '@/auth';

// Sign in action
export async function handleSignIn(provider: string) {
  await signIn(provider, { redirectTo: '/dashboard' });
}

// Sign out action
export async function handleSignOut() {
  await signOut({ redirectTo: '/' });
}

// Protected action
export async function createPost(formData: FormData) {
  const session = await auth();

  if (!session?.user) {
    throw new Error('Unauthorized');
  }

  const title = formData.get('title') as string;

  await prisma.post.create({
    data: {
      title,
      authorId: session.user.id,
    },
  });

  revalidatePath('/posts');
}

API ルートの保護

import { auth } from '@/auth';
import { NextResponse } from 'next/server';

export async function GET() {
  const session = await auth();

  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const data = await fetchUserData(session.user.id);
  return NextResponse.json(data);
}

ロールベースのアクセス制御

型の拡張
// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from 'next-auth';
import { JWT, DefaultJWT } from 'next-auth/jwt';

declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      role: string;
    } & DefaultSession['user'];
  }

  interface User extends DefaultUser {
    role: string;
  }
}

declare module 'next-auth/jwt' {
  interface JWT extends DefaultJWT {
    id: string;
    role: string;
  }
}
ロール確認ユーティリティ
import { auth } from '@/auth';

export async function requireRole(allowedRoles: string[]) {
  const session = await auth();

  if (!session?.user) {
    throw new Error('Unauthorized');
  }

  if (!allowedRoles.includes(session.user.role)) {
    throw new Error('Forbidden');
  }

  return session;
}

// Usage
export default async function AdminPage() {
  const session = await requireRole(['admin']);

  return <AdminDashboard user={session.user} />;
}

OAuth プロバイダーの設定

GitHub
import GitHub from 'next-auth/providers/github';

GitHub({
  clientId: process.env.AUTH_GITHUB_ID,
  clientSecret: process.env.AUTH_GITHUB_SECRET,
  authorization: {
    params: {
      scope: 'read:user user:email',
    },
  },
});
Google
import Google from 'next-auth/providers/google';

Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  authorization: {
    params: {
      prompt: 'consent',
      access_type: 'offline',
      response_type: 'code',
    },
  },
});
認証情報プロバイダー
import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';

Credentials({
  name: 'credentials',
  credentials: {
    email: { label: 'Email', type: 'email' },
    password: { label: 'Password', type: 'password' },
  },
  async authorize(credentials) {
    if (!credentials?.email || !credentials?.password) {
      return null;
    }

    const user = await prisma.user.findUnique({
      where: { email: credentials.email },
    });

    if (!user || !user.hashedPassword) {
      return null;
    }

    const isValid = await bcrypt.compare(credentials.password, user.hashedPassword);

    if (!isValid) {
      return null;
    }

    return {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    };
  },
});

セキュリティのベストプラクティス

1. 本番環境では必ず AUTH_SECRET を使用する
# Generate a secure secret
openssl rand -base64 32
2. Cookie の設定
export const { auth } = NextAuth({
  cookies: {
    sessionToken: {
      name: `__Secure-authjs.session-token`,
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: process.env.NODE_ENV === 'production',
      },
    },
  },
});
3. CSRF 保護

Auth.js は CSRF 保護を自動的に処理します。以下を徹底してください。

  • サインイン/サインアウトには POST を使用します
  • 組み込みの保護機能を無効にしないでください
4. サーバー側でセッションを検証する
// Always verify on the server for sensitive operations
export async function sensitiveOperation() {
  const session = await auth();

  if (!session?.user) {
    throw new Error('Unauthorized');
  }

  // Double-check user exists in database
  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
  });

  if (!user || user.banned) {
    throw new Error('Access denied');
  }

  // Proceed with operation
}

セッションの更新とポーリング

'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider
      refetchInterval={5 * 60} // Refetch every 5 minutes
      refetchOnWindowFocus={true}
    >
      {children}
    </SessionProvider>
  );
}

エラー処理

カスタムエラーページ
// app/auth/error/page.tsx
export default function AuthErrorPage({
  searchParams,
}: {
  searchParams: { error?: string };
}) {
  const errorMessages: Record<string, string> = {
    Configuration: 'There is a problem with the server configuration.',
    AccessDenied: 'You do not have permission to sign in.',
    Verification: 'The verification link has expired or has already been used.',
    Default: 'An error occurred during authentication.',
  };

  const error = searchParams.error || 'Default';
  const message = errorMessages[error] || errorMessages.Default;

  return (
    <div>
      <h1>Authentication Error</h1>
      <p>{message}</p>
      <a href="/auth/signin">Try again</a>
    </div>
  );
}

テスト

// Mock auth for testing
import { auth } from '@/auth';

jest.mock('@/auth', () => ({
  auth: jest.fn(),
}));

describe('Protected API', () => {
  it('returns 401 for unauthenticated requests', async () => {
    (auth as jest.Mock).mockResolvedValue(null);

    const response = await GET();
    expect(response.status).toBe(401);
  });

  it('returns data for authenticated requests', async () => {
    (auth as jest.Mock).mockResolvedValue({
      user: { id: '1', email: '[email protected]' },
    });

    const response = await GET();
    expect(response.status).toBe(200);
  });
});

よくある問題と解決策

更新時にセッションが消える
  1. 本番環境で AUTH_SECRET が設定されていることを確認します
  2. Cookie の設定を確認します
  3. 本番環境で HTTPS が有効になっていることを確認します
Edge ランタイムとの互換性

Edge ミドルウェアでデータベースアダプターを使用する場合は、分割設定を使用します。

カスタムプロパティの型エラー

types/next-auth.d.ts で型を拡張します。

避けるべき一般的なアンチパターン

  1. NEXTAUTH_ プレフィックスを使用する(v5 では AUTH_ を使用します)
  2. 本番環境で AUTH_SECRET を設定しない
  3. 認可をクライアント側のセッション確認に依存する
  4. クライアントコンポーネントでローディング状態を処理しない
  5. Edge ミドルウェアでデータベース戦略を使用する
  6. サーバーアクションでセッションを検証しない
  7. JWT トークンで機密データを公開する

コメント