Перейти к содержимому

8. 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 из коробки
Окно терминала
npm install next-auth@beta
Окно терминала
# Генерация AUTH_SECRET
openssl 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!,
}),
],
});
app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
.env.local
AUTH_SECRET=your-generated-secret
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_ID=...
GITHUB_SECRET=...
NEXTAUTH_URL=http://localhost:3000
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',
},
});
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 дней
},
});
types/next-auth.d.ts
// Расширяем типы TypeScript
import '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;
}
}
app/dashboard/page.tsx
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>
);
}
'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>
);
}
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 }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
app/api/posts/route.ts
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.ts
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).*)'],
};
Окно терминала
npm install @auth/prisma-adapter
prisma/schema.prisma
model 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: [/* ... */],
});
app/login/page.tsx
'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>
);
}
  1. Настрой NextAuth.js с Google и GitHub провайдерами
  2. Добавь Credentials провайдер с bcrypt
  3. Настрой Prisma адаптер для хранения сессий в БД
  4. Реализуй middleware для защиты /dashboard маршрутов
  5. Добавь кастомную страницу логина