nextauth について
Next.js 開発者向けの NextAuth.js 認証スキルです。App Router の設定、クライアントとサーバーでのセッション処理、本番環境のセキュリティ対策を扱い、トラブルシューティングとテスト例も含みます。
主な機能
- 01App Router の認証設定
- 02クライアントとサーバーのセッション処理
- 03Provider と Adapter の例
- 04本番環境のセキュリティ指針
- 05トラブルシューティングとテスト例
活用シーン
- 01Next.js アプリへのサインイン追加
- 02サーバー側でのページ保護
- 03認証設定やセッション問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill nextauthインストール後、Claude Code で/nextauthを入力して読み込まれたか確認します。
同梱ファイルquick-ref/callbacks.mdquick-ref/setup.md
詳細
NextAuth.js のコア知識
詳細な知識:包括的なドキュメントについては、テクノロジーに
nextauthを指定してmcp__documentation__fetch_docsを使用してください。
基本的なセットアップ(App Router)
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
// lib/auth.ts
import { NextAuthOptions } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from './prisma';
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials?.email },
});
if (user && await verifyPassword(credentials?.password, user.password)) {
return user;
}
return null;
},
}),
],
callbacks: {
async session({ session, user }) {
session.user.id = user.id;
return session;
},
},
pages: {
signIn: '/login',
error: '/auth/error',
},
};
クライアントでの使用
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
function AuthButton() {
const { data: session, status } = useSession();
if (status === 'loading') return <Spinner />;
if (session) {
return (
<div>
<span>{session.user?.email}</span>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
return <button onClick={() => signIn('google')}>Sign in</button>;
}
サーバーサイド認証
// In Server Component
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
async function ProtectedPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/login');
}
return <div>Welcome {session.user.name}</div>;
}
このスキルを使用しない場合
- 汎用 OAuth 2.0 フロー - プラットフォームに依存しない OAuth には
oauth2スキルを使用してください - カスタム JWT 実装 - カスタムトークンロジックには
jwtスキルを使用してください - Next.js 以外のフレームワーク - 各フレームワーク固有の認証を使用してください(Express Passport など)
- Remix/SvelteKit - それぞれのネイティブ認証ソリューションを使用してください
型の拡張
// types/next-auth.d.ts
declare module 'next-auth' {
interface Session {
user: { id: string; role: string } & DefaultSession['user'];
}
}
アンチパターン
| アンチパターン | 問題点 | 正しい方法 |
|---|---|---|
| NEXTAUTH_SECRET を設定しない | セキュリティの脆弱性 | 本番環境では必ず設定してください |
| クライアント側のセッション確認のみ | 回避される可能性があります | getServerSession() を使用してください |
| プロバイダーの認証情報をハードコードする | セキュリティ上のリスク | 環境変数を使用してください |
| エラーハンドリングがない | UX が低下します | カスタムエラーページを実装してください |
| セッション戦略を混在させる | 動作が一貫しなくなります | JWT またはデータベースのどちらかに統一してください |
| CSRF 保護がない | 攻撃に対して脆弱になります | デフォルトの CSRF 保護を使用してください(デフォルトで有効) |
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| "Configuration error" | 必須の環境変数が不足しています | NEXTAUTH_URL と NEXTAUTH_SECRET を確認してください |
| セッションが null | 認証されていないか、セッションの有効期限が切れています | signIn() が呼び出されたことを確認してください |
| "Callback URL error" | リダイレクト先が無効です | プロバイダーの設定で URL を許可リストに追加してください |
| 型エラー | 型の拡張が不足しています | types/next-auth.d.ts を作成してください |
| セッションが更新されない | キャッシュの問題です | useSession から update() を呼び出してください |
| CORS エラー | ドメインが間違っています | NEXTAUTH_URL がデプロイ URL と一致することを確認してください |
本番環境への対応
セキュリティ設定
// lib/auth.ts
import { NextAuthOptions } from 'next-auth';
export const authOptions: NextAuthOptions = {
// Secure session configuration
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
// Secure cookies
cookies: {
sessionToken: {
name: process.env.NODE_ENV === 'production'
? '__Secure-next-auth.session-token'
: 'next-auth.session-token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
},
},
},
// Callbacks for security
callbacks: {
async jwt({ token, user, account }) {
if (user) {
token.id = user.id;
token.role = user.role;
}
return token;
},
async session({ session, token }) {
session.user.id = token.id as string;
session.user.role = token.role as string;
return session;
},
async signIn({ user, account, profile }) {
// Block suspicious sign-ins
const isAllowed = await checkUserAllowed(user.email);
return isAllowed;
},
},
// Security events
events: {
async signIn({ user, account }) {
await logSecurityEvent('signin', { userId: user.id, provider: account?.provider });
},
async signOut({ token }) {
await logSecurityEvent('signout', { userId: token.sub });
},
},
};
レート制限
// middleware.ts
import { NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 requests per minute
});
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/auth')) {
const ip = request.ip ?? '127.0.0.1';
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
if (!success) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
});
}
}
return NextResponse.next();
}
CSRF 保護
// lib/auth.ts
export const authOptions: NextAuthOptions = {
// Enable CSRF token verification
useSecureCookies: process.env.NODE_ENV === 'production',
// Custom CSRF token
callbacks: {
async redirect({ url, baseUrl }) {
// Only allow redirects to same origin
if (url.startsWith('/')) return `${baseUrl}${url}`;
if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
};
// In API routes, verify CSRF
import { getToken } from 'next-auth/jwt';
export async function POST(request: Request) {
const token = await getToken({ req: request });
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
// Process request
}
エラーハンドリング
// app/auth/error/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
const errorMessages: Record<string, string> = {
Configuration: 'Server configuration error',
AccessDenied: 'Access denied',
Verification: 'Verification link expired',
Default: 'Authentication error',
};
export default function AuthError() {
const searchParams = useSearchParams();
const error = searchParams.get('error') ?? 'Default';
return (
<div className="error-page">
<h1>Authentication Error</h1>
<p>{errorMessages[error] ?? errorMessages.Default}</p>
<a href="/login">Try again</a>
</div>
);
}
テスト
// __tests__/auth.test.ts
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
// Mock next-auth
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}));
describe('Protected Route', () => {
it('redirects unauthenticated users', async () => {
(getServerSession as jest.Mock).mockResolvedValue(null);
const response = await fetch('/api/protected');
expect(response.status).toBe(401);
});
it('allows authenticated users', async () => {
(getServerSession as jest.Mock).mockResolvedValue({
user: { id: '1', email: '[email protected]', role: 'user' },
});
const response = await fetch('/api/protected');
expect(response.status).toBe(200);
});
});
// E2E with Playwright
test('OAuth flow', async ({ page }) => {
await page.goto('/login');
await page.click('button:has-text("Sign in with Google")');
// Mock OAuth provider response in test environment
await expect(page).toHaveURL('/dashboard');
});
監視メトリクス
| メトリクス | 目標 |
|---|---|
| ログイン成功率 | > 99% |
| 認証レイテンシ | < 200ms |
| ログイン失敗試行回数 | 監視してアラートを送信 |
| トークン更新成功率 | > 99.9% |
チェックリスト
- セキュアな Cookie 設定
- 適切な maxAge を設定した JWT
- 認証エンドポイントでのレート制限
- CSRF 保護の有効化
- リダイレクト URL の検証
- セキュリティイベントのログ記録
- カスタムエラーページ
- セッション更新戦略
- ロールベースのアクセス制御
- モックセッションを使用したテスト
参考ドキュメント
コメント
Markdown 対応