8. NextAuth.js
Что такое NextAuth.js?
Заголовок раздела «Что такое NextAuth.js?»NextAuth.js (v5 = Auth.js) — готовая библиотека аутентификации для Next.js. Берёт на себя весь OAuth 2.0 / OIDC flow.
Что умеет:
- 50+ OAuth провайдеров (Google, GitHub, Twitter, …)
- Email/password аутентификация (Credentials)
- Magic Links (Email)
- Сессии через JWT или Database
- TypeScript из коробки
Установка (NextAuth v5)
Заголовок раздела «Установка (NextAuth v5)»npm install next-auth@beta# Генерация AUTH_SECRETopenssl rand -base64 32Базовая настройка
Заголовок раздела «Базовая настройка»// auth.ts (в корне проекта)import NextAuth from 'next-auth';import Google from 'next-auth/providers/google';import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), ],});import { handlers } from '@/auth';export const { GET, POST } = handlers;AUTH_SECRET=your-generated-secretGOOGLE_CLIENT_ID=...GOOGLE_CLIENT_SECRET=...GITHUB_ID=...GITHUB_SECRET=...
NEXTAUTH_URL=http://localhost:3000Credentials Provider (email + password)
Заголовок раздела «Credentials Provider (email + password)»import NextAuth from 'next-auth';import Credentials from 'next-auth/providers/credentials';import bcrypt from 'bcryptjs';import { db } from '@/lib/db';
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ 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 db.user.findUnique({ where: { email: credentials.email as string }, });
if (!user || !user.password) return null;
const isValid = await bcrypt.compare( credentials.password as string, user.password );
if (!isValid) return null;
return { id: user.id, email: user.email, name: user.name, role: user.role, }; }, }), ], pages: { signIn: '/login', // кастомная страница логина error: '/auth/error', },});Кастомизация session и JWT
Заголовок раздела «Кастомизация session и JWT»export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [/* ... */],
callbacks: { // Расширяем JWT дополнительными данными async jwt({ token, user, account }) { if (user) { // При первом логине user содержит данные token.id = user.id; token.role = user.role; }
if (account?.provider === 'google') { token.googleId = account.providerAccountId; }
return token; },
// Расширяем session объект async session({ session, token }) { if (token) { session.user.id = token.id as string; session.user.role = token.role as string; } return session; }, },
session: { strategy: 'jwt', // или 'database' maxAge: 30 * 24 * 60 * 60, // 30 дней },});// Расширяем типы TypeScriptimport 'next-auth';
declare module 'next-auth' { interface Session { user: { id: string; role: string; } & DefaultSession['user']; }
interface User { role?: string; }}
declare module 'next-auth/jwt' { interface JWT { id?: string; role?: string; }}Использование в компонентах
Заголовок раздела «Использование в компонентах»Server Components
Заголовок раздела «Server Components»import { auth } from '@/auth';import { redirect } from 'next/navigation';
export default async function DashboardPage() { const session = await auth();
if (!session?.user) { redirect('/login'); }
return ( <div> <h1>Привет, {session.user.name}!</h1> <p>Email: {session.user.email}</p> <p>Роль: {session.user.role}</p> </div> );}Client Components
Заголовок раздела «Client Components»'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
export function UserMenu() { const { data: session, status } = useSession();
if (status === 'loading') return <div>Loading...</div>;
if (!session) { return ( <button onClick={() => signIn('google')}> Войти через Google </button> ); }
return ( <div> <img src={session.user.image || ''} alt={session.user.name || ''} /> <span>{session.user.name}</span> <button onClick={() => signOut({ callbackUrl: '/' })}> Выйти </button> </div> );}Provider для Client Components
Заголовок раздела «Provider для Client Components»'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider>;}
// app/layout.tsximport { Providers } from './providers';
export default function RootLayout({ children }) { return ( <html> <body> <Providers>{children}</Providers> </body> </html> );}Защита API Routes
Заголовок раздела «Защита API Routes»import { auth } from '@/auth';import { NextResponse } from 'next/server';
export async function GET(req: Request) { const session = await auth();
if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }
const posts = await db.post.findMany({ where: { authorId: session.user.id }, });
return NextResponse.json(posts);}Middleware для защиты страниц
Заголовок раздела «Middleware для защиты страниц»import { auth } from '@/auth';
export default auth((req) => { const isAuthenticated = !!req.auth;
// Защищённые пути const protectedPaths = ['/dashboard', '/profile', '/admin']; const isProtected = protectedPaths.some(path => req.nextUrl.pathname.startsWith(path) );
if (isProtected && !isAuthenticated) { const loginUrl = new URL('/login', req.url); loginUrl.searchParams.set('callbackUrl', req.url); return Response.redirect(loginUrl); }
// Только для авторизованных if (req.nextUrl.pathname.startsWith('/admin')) { if (req.auth?.user?.role !== 'admin') { return Response.redirect(new URL('/403', req.url)); } }});
export const config = { matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'],};Database Adapter (Prisma)
Заголовок раздела «Database Adapter (Prisma)»npm install @auth/prisma-adaptermodel 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? role String @default("user") accounts Account[] sessions Session[]}
model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token])}// auth.ts с Prisma адаптеромimport { PrismaAdapter } from '@auth/prisma-adapter';import { prisma } from '@/lib/prisma';
export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), session: { strategy: 'database' }, // теперь сессии в БД providers: [/* ... */],});Кастомная страница логина
Заголовок раздела «Кастомная страница логина»'use client';
import { signIn } from 'next-auth/react';import { useSearchParams } from 'next/navigation';
export default function LoginPage() { const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl') || '/dashboard'; const error = searchParams.get('error');
return ( <div className="flex min-h-screen items-center justify-center"> <div className="w-full max-w-md space-y-4 p-8"> <h1 className="text-2xl font-bold">Вход</h1>
{error && ( <div className="rounded bg-red-100 p-3 text-red-700"> {error === 'CredentialsSignin' ? 'Неверный email или пароль' : error} </div> )}
<button onClick={() => signIn('google', { callbackUrl })} className="flex w-full items-center justify-center gap-3 rounded border p-3" > <GoogleIcon /> Войти через Google </button>
<button onClick={() => signIn('github', { callbackUrl })} className="flex w-full items-center justify-center gap-3 rounded border p-3" > <GitHubIcon /> Войти через GitHub </button> </div> </div> );}Практические задания
Заголовок раздела «Практические задания»- Настрой NextAuth.js с Google и GitHub провайдерами
- Добавь Credentials провайдер с bcrypt
- Настрой Prisma адаптер для хранения сессий в БД
- Реализуй middleware для защиты /dashboard маршрутов
- Добавь кастомную страницу логина