Next.js Authentication: NextAuth.js ile Güvenli Oturum Yönetimi Rehberi
Next.js 15 ile NextAuth.js (Auth.js) kullanarak production-ready authentication sistemi kurun. OAuth providers, credentials auth, session yönetimi ve güvenlik best practices bu kapsamlı rehberde.
Next.js Authentication: NextAuth.js ile Güvenli Oturum Yönetimi Rehberi
Authentication, her web uygulamasının temel taşlarından biri. Kullanıcıların güvenli şekilde giriş yapması, oturumlarının yönetilmesi ve yetkilerinin kontrol edilmesi kritik öneme sahip. NextAuth.js (artık Auth.js olarak da biliniyor), Next.js için tasarlanmış en popüler authentication çözümü.
Bu rehberde NextAuth.js v5'i Next.js 15 App Router ile entegre edeceğiz. Google, GitHub gibi OAuth provider'lardan email/password authentication'a, session yönetiminden middleware korumasına kadar tüm konuları ele alacağız.
İçindekiler
- NextAuth.js Nedir ve Neden Kullanmalı?
- Kurulum ve Temel Konfigürasyon
- OAuth Providers Entegrasyonu
- Credentials Provider ile Email/Password
- Session Yönetimi
- Middleware ile Route Koruması
- Server Components'ta Auth Kullanımı
- Client Components'ta Auth Kullanımı
- Database Adapter Entegrasyonu
- Role-Based Access Control
- Özel Login ve Register Sayfaları
- Email Verification
- Password Reset Flow
- Güvenlik Best Practices
- Sık Sorulan Sorular
NextAuth.js Nedir ve Neden Kullanmalı?
NextAuth.js, Next.js için özel olarak tasarlanmış, open-source authentication kütüphanesi. 2024'ten itibaren Auth.js olarak rebrand edildi ve framework-agnostic hale geldi, ancak Next.js desteği hâlâ birincil odak.
Avantajları
Kolay Entegrasyon: Birkaç satır kodla OAuth, email veya credentials authentication ekleyebilirsiniz.
Güvenlik: CSRF koruması, secure cookies, JWT encryption gibi güvenlik önlemleri built-in.
Esneklik: 50+ OAuth provider desteği, custom provider oluşturma imkanı.
Database Agnostic: Prisma, Drizzle, TypeORM gibi adaptörlerle tüm veritabanlarını destekler.
Alternatifler
| Çözüm | Artılar | Eksiler |
|---|---|---|
| NextAuth.js | Free, flexible, self-hosted | Learning curve |
| Clerk | Hosted, feature-rich | Ücretli, vendor lock-in |
| Auth0 | Enterprise-grade | Karmaşık, pahalı |
| Supabase Auth | Kolay, database entegre | Supabase'e bağımlı |
| Lucia | Minimal, type-safe | Daha az abstraction |
Kurulum ve Temel Konfigürasyon
Paketlerin Kurulumu
npm install next-auth@beta @auth/prisma-adapter
npm install bcryptjs
npm install -D @types/bcryptjsAuth Configuration
// auth.ts
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
import { compare } from 'bcryptjs'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: 'jwt' },
pages: {
signIn: '/login',
error: '/auth/error'
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!
}),
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 as string }
})
if (!user || !user.password) {
return null
}
const isValid = await compare(
credentials.password as string,
user.password
)
if (!isValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
role: user.role
}
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
session.user.role = token.role as string
}
return session
}
}
})Route Handler
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlersEnvironment Variables
# .env.local
AUTH_SECRET="your-secret-key-min-32-characters"
AUTH_URL="http://localhost:3000"
# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# GitHub OAuth
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"# AUTH_SECRET oluşturma
npx auth secretTypeScript Types
// 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
}
}OAuth Providers Entegrasyonu
Google OAuth Setup
- Google Cloud Console'a gidin
- Yeni proje oluşturun veya mevcut projeyi seçin
- APIs & Services > Credentials
- Create Credentials > OAuth 2.0 Client IDs
- Application type: Web application
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google
// auth.ts
import Google from 'next-auth/providers/google'
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code'
}
}
})
]GitHub OAuth Setup
- GitHub Settings > Developer settings > OAuth Apps
- New OAuth App
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github
import GitHub from 'next-auth/providers/github'
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!
})
]Diğer Popüler Provider'lar
import Discord from 'next-auth/providers/discord'
import Twitter from 'next-auth/providers/twitter'
import LinkedIn from 'next-auth/providers/linkedin'
import Apple from 'next-auth/providers/apple'
providers: [
Discord({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!
}),
Twitter({
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!
}),
LinkedIn({
clientId: process.env.LINKEDIN_CLIENT_ID!,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET!
})
]Credentials Provider ile Email/Password
Prisma Schema
// prisma/schema.prisma
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
password String?
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
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 VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}Register Action
// app/actions/auth.ts
'use server'
import { prisma } from '@/lib/prisma'
import { hash } from 'bcryptjs'
import { z } from 'zod'
const registerSchema = z.object({
name: z.string().min(2, 'İsim en az 2 karakter olmalı'),
email: z.string().email('Geçerli email adresi girin'),
password: z
.string()
.min(8, 'Şifre en az 8 karakter olmalı')
.regex(/[A-Z]/, 'En az bir büyük harf içermeli')
.regex(/[0-9]/, 'En az bir rakam içermeli')
})
export async function register(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password')
}
const result = registerSchema.safeParse(rawData)
if (!result.success) {
return {
error: 'Validasyon hatası',
errors: result.error.flatten().fieldErrors
}
}
const { name, email, password } = result.data
// Email kontrolü
const existingUser = await prisma.user.findUnique({
where: { email }
})
if (existingUser) {
return { error: 'Bu email adresi zaten kullanılıyor' }
}
// Şifreyi hashle
const hashedPassword = await hash(password, 12)
// Kullanıcı oluştur
try {
await prisma.user.create({
data: {
name,
email,
password: hashedPassword
}
})
return { success: true }
} catch (error) {
return { error: 'Kayıt oluşturulamadı' }
}
}Login Action
// app/actions/auth.ts
'use server'
import { signIn } from '@/auth'
import { AuthError } from 'next-auth'
export async function login(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
try {
await signIn('credentials', {
email,
password,
redirectTo: '/dashboard'
})
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return { error: 'Email veya şifre hatalı' }
default:
return { error: 'Giriş yapılamadı' }
}
}
throw error
}
}
export async function loginWithGoogle() {
await signIn('google', { redirectTo: '/dashboard' })
}
export async function loginWithGitHub() {
await signIn('github', { redirectTo: '/dashboard' })
}
export async function logout() {
await signOut({ redirectTo: '/' })
}Session Yönetimi
JWT vs Database Sessions
// JWT Strategy (Varsayılan)
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 gün
updateAge: 24 * 60 * 60 // 24 saatte bir güncelle
}
// Database Strategy
session: {
strategy: 'database',
maxAge: 30 * 24 * 60 * 60,
updateAge: 24 * 60 * 60
}JWT Avantajları:
- Stateless, database sorgusu yok
- Edge runtime uyumlu
- Scalable
Database Avantajları:
- Session'ı anında iptal edebilme
- Tüm aktif session'ları görme
- Daha güvenli (token theft'e karşı)
Session Callbacks
callbacks: {
async jwt({ token, user, account, profile, trigger, session }) {
// İlk login'de user bilgilerini token'a ekle
if (user) {
token.id = user.id
token.role = user.role
}
// Session update trigger'ı
if (trigger === 'update' && session) {
token.name = session.name
}
return token
},
async session({ session, token, user }) {
// Token bilgilerini session'a aktar
if (session.user) {
session.user.id = token.id as string
session.user.role = token.role as string
}
return session
},
async signIn({ user, account, profile }) {
// Login'i engellemek için false dön
if (user.email?.endsWith('@blocked-domain.com')) {
return false
}
// Email verification kontrolü
if (!user.emailVerified && account?.provider === 'credentials') {
return '/auth/verify-email'
}
return true
},
async redirect({ url, baseUrl }) {
// Relative URL'leri handle et
if (url.startsWith('/')) {
return `${baseUrl}${url}`
}
// Aynı origin ise izin ver
if (new URL(url).origin === baseUrl) {
return url
}
return baseUrl
}
}Middleware ile Route Koruması
Temel Middleware
// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isPublicRoute = [
'/',
'/login',
'/register',
'/api/auth'
].some(route => nextUrl.pathname.startsWith(route))
const isAuthRoute = ['/login', '/register'].includes(nextUrl.pathname)
// Auth sayfalarına giriş yapmış kullanıcı erişmesin
if (isAuthRoute && isLoggedIn) {
return NextResponse.redirect(new URL('/dashboard', nextUrl))
}
// Protected route'lara giriş yapmamış kullanıcı erişmesin
if (!isPublicRoute && !isLoggedIn) {
const callbackUrl = encodeURIComponent(nextUrl.pathname)
return NextResponse.redirect(
new URL(`/login?callbackUrl=${callbackUrl}`, nextUrl)
)
}
return NextResponse.next()
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\..*).*)']
}Role-Based Middleware
// middleware.ts
import { auth } from '@/auth'
import { NextResponse } from 'next/server'
const roleRoutes = {
admin: ['/admin', '/admin/users', '/admin/settings'],
author: ['/dashboard/posts/new', '/dashboard/posts/edit']
}
export default auth((req) => {
const { nextUrl } = req
const user = req.auth?.user
// Admin route kontrolü
if (nextUrl.pathname.startsWith('/admin')) {
if (!user || user.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', nextUrl))
}
}
// Author route kontrolü
const isAuthorRoute = roleRoutes.author.some(route =>
nextUrl.pathname.startsWith(route)
)
if (isAuthorRoute) {
if (!user || !['admin', 'author'].includes(user.role)) {
return NextResponse.redirect(new URL('/unauthorized', nextUrl))
}
}
return NextResponse.next()
})Server Components'ta Auth Kullanımı
Session Alma
// app/dashboard/page.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div>
<h1>Hoş geldin, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
<p>Rol: {session.user.role}</p>
</div>
)
}Protected Layout
// app/(protected)/layout.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
export default async function ProtectedLayout({
children
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div className="flex">
<Sidebar user={session.user} />
<main className="flex-1">{children}</main>
</div>
)
}Auth Helper Functions
// lib/auth-utils.ts
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
export async function requireAuth() {
const session = await auth()
if (!session) {
redirect('/login')
}
return session
}
export async function requireRole(role: string | string[]) {
const session = await requireAuth()
const roles = Array.isArray(role) ? role : [role]
if (!roles.includes(session.user.role)) {
redirect('/unauthorized')
}
return session
}
// Kullanım
export default async function AdminPage() {
const session = await requireRole('admin')
// ...
}Client Components'ta Auth Kullanımı
SessionProvider
// 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="tr">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}useSession Hook
'use client'
import { useSession, signIn, signOut } from 'next-auth/react'
export function UserMenu() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Yükleniyor...</div>
}
if (!session) {
return (
<button onClick={() => signIn()}>
Giriş Yap
</button>
)
}
return (
<div className="flex items-center gap-4">
<img
src={session.user.image || '/default-avatar.png'}
alt={session.user.name || 'User'}
className="w-8 h-8 rounded-full"
/>
<span>{session.user.name}</span>
<button onClick={() => signOut()}>
Çıkış Yap
</button>
</div>
)
}Session Update
'use client'
import { useSession } from 'next-auth/react'
export function ProfileForm() {
const { data: session, update } = useSession()
async function handleSubmit(formData: FormData) {
const name = formData.get('name') as string
// API'ye güncelleme isteği
await fetch('/api/user/profile', {
method: 'PATCH',
body: JSON.stringify({ name })
})
// Session'ı güncelle
await update({ name })
}
return (
<form action={handleSubmit}>
<input
name="name"
defaultValue={session?.user.name || ''}
/>
<button type="submit">Güncelle</button>
</form>
)
}Database Adapter Entegrasyonu
Prisma Adapter
// auth.ts
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
// Credentials provider kullanıyorsanız JWT zorunlu
session: { strategy: 'jwt' },
// ...
})Drizzle Adapter
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { db } from '@/lib/drizzle'
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: DrizzleAdapter(db),
// ...
})Custom User Fields
// Prisma schema'ya custom field ekle
model User {
// ...
role String @default("user")
bio String?
subscription String @default("free")
}
// Callbacks'te session'a ekle
callbacks: {
async session({ session, token }) {
if (session.user) {
const user = await prisma.user.findUnique({
where: { id: token.id as string },
select: { role: true, subscription: true }
})
session.user.id = token.id as string
session.user.role = user?.role || 'user'
session.user.subscription = user?.subscription || 'free'
}
return session
}
}Role-Based Access Control
RBAC Implementation
// lib/permissions.ts
export const ROLES = {
USER: 'user',
AUTHOR: 'author',
ADMIN: 'admin'
} as const
export type Role = typeof ROLES[keyof typeof ROLES]
export const PERMISSIONS = {
// Posts
'posts:read': ['user', 'author', 'admin'],
'posts:create': ['author', 'admin'],
'posts:update': ['author', 'admin'],
'posts:delete': ['admin'],
// Users
'users:read': ['admin'],
'users:update': ['admin'],
'users:delete': ['admin'],
// Settings
'settings:read': ['admin'],
'settings:update': ['admin']
} as const
export type Permission = keyof typeof PERMISSIONS
export function hasPermission(
role: Role,
permission: Permission
): boolean {
return PERMISSIONS[permission].includes(role)
}
export function requirePermission(
role: Role,
permission: Permission
): void {
if (!hasPermission(role, permission)) {
throw new Error('Yetkisiz erişim')
}
}Permission Check Hook
'use client'
import { useSession } from 'next-auth/react'
import { hasPermission, Permission } from '@/lib/permissions'
export function usePermission(permission: Permission): boolean {
const { data: session } = useSession()
if (!session?.user.role) {
return false
}
return hasPermission(session.user.role as Role, permission)
}
// Kullanım
export function DeleteButton({ postId }: { postId: string }) {
const canDelete = usePermission('posts:delete')
if (!canDelete) {
return null
}
return (
<button onClick={() => deletePost(postId)}>
Sil
</button>
)
}Server-Side Permission Check
// app/actions/posts.ts
'use server'
import { auth } from '@/auth'
import { requirePermission } from '@/lib/permissions'
export async function deletePost(postId: string) {
const session = await auth()
if (!session) {
return { error: 'Oturum açmanız gerekiyor' }
}
try {
requirePermission(session.user.role, 'posts:delete')
} catch {
return { error: 'Bu işlem için yetkiniz yok' }
}
await prisma.post.delete({ where: { id: postId } })
revalidatePath('/posts')
return { success: true }
}Özel Login ve Register Sayfaları
Login Page
// app/login/page.tsx
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { LoginForm } from '@/components/auth/login-form'
import { OAuthButtons } from '@/components/auth/oauth-buttons'
export default async function LoginPage({
searchParams
}: {
searchParams: Promise<{ callbackUrl?: string; error?: string }>
}) {
const session = await auth()
const { callbackUrl, error } = await searchParams
if (session) {
redirect(callbackUrl || '/dashboard')
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md p-8 space-y-6 bg-white rounded-lg shadow">
<h1 className="text-2xl font-bold text-center">Giriş Yap</h1>
{error && (
<div className="p-3 bg-red-100 text-red-800 rounded">
{error === 'OAuthAccountNotLinked'
? 'Bu email başka bir yöntemle kayıtlı'
: 'Giriş yapılamadı'}
</div>
)}
<OAuthButtons />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">veya</span>
</div>
</div>
<LoginForm callbackUrl={callbackUrl} />
<p className="text-center text-sm text-gray-600">
Hesabınız yok mu?{' '}
<a href="/register" className="text-primary hover:underline">
Kayıt Ol
</a>
</p>
</div>
</div>
)
}Login Form Component
// components/auth/login-form.tsx
'use client'
import { useActionState } from 'react'
import { login } from '@/app/actions/auth'
export function LoginForm({ callbackUrl }: { callbackUrl?: string }) {
const [state, formAction, isPending] = useActionState(login, null)
return (
<form action={formAction} className="space-y-4">
<input type="hidden" name="callbackUrl" value={callbackUrl || '/dashboard'} />
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="w-full mt-1 px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Şifre
</label>
<input
id="password"
name="password"
type="password"
required
className="w-full mt-1 px-3 py-2 border rounded-lg"
/>
</div>
{state?.error && (
<div className="p-3 bg-red-100 text-red-800 rounded text-sm">
{state.error}
</div>
)}
<button
type="submit"
disabled={isPending}
className="w-full py-2 bg-primary text-white rounded-lg disabled:opacity-50"
>
{isPending ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
<div className="text-center">
<a href="/forgot-password" className="text-sm text-gray-600 hover:underline">
Şifremi Unuttum
</a>
</div>
</form>
)
}OAuth Buttons
// components/auth/oauth-buttons.tsx
'use client'
import { loginWithGoogle, loginWithGitHub } from '@/app/actions/auth'
export function OAuthButtons() {
return (
<div className="space-y-3">
<form action={loginWithGoogle}>
<button
type="submit"
className="w-full flex items-center justify-center gap-3 py-2 border rounded-lg hover:bg-gray-50"
>
<GoogleIcon className="w-5 h-5" />
Google ile devam et
</button>
</form>
<form action={loginWithGitHub}>
<button
type="submit"
className="w-full flex items-center justify-center gap-3 py-2 border rounded-lg hover:bg-gray-50"
>
<GitHubIcon className="w-5 h-5" />
GitHub ile devam et
</button>
</form>
</div>
)
}Email Verification
Verification Token Schema
model VerificationToken {
id String @id @default(cuid())
email String
token String @unique
expires DateTime
@@unique([email, token])
}Send Verification Email
// lib/email.ts
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function sendVerificationEmail(email: string, token: string) {
const verifyUrl = `${process.env.AUTH_URL}/auth/verify?token=${token}`
await resend.emails.send({
from: 'noreply@example.com',
to: email,
subject: 'Email Adresinizi Doğrulayın',
html: `
<h1>Email Doğrulama</h1>
<p>Hesabınızı doğrulamak için aşağıdaki bağlantıya tıklayın:</p>
<a href="${verifyUrl}">Email Adresimi Doğrula</a>
<p>Bu bağlantı 24 saat geçerlidir.</p>
`
})
}
// app/actions/auth.ts
export async function register(formData: FormData) {
// ... validation
const user = await prisma.user.create({
data: { name, email, password: hashedPassword }
})
// Verification token oluştur
const token = crypto.randomUUID()
await prisma.verificationToken.create({
data: {
email,
token,
expires: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 saat
}
})
await sendVerificationEmail(email, token)
return { success: true, message: 'Doğrulama emaili gönderildi' }
}Verify Email Page
// app/auth/verify/page.tsx
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'
export default async function VerifyPage({
searchParams
}: {
searchParams: Promise<{ token?: string }>
}) {
const { token } = await searchParams
if (!token) {
return <div>Geçersiz token</div>
}
const verificationToken = await prisma.verificationToken.findUnique({
where: { token }
})
if (!verificationToken || verificationToken.expires < new Date()) {
return <div>Token geçersiz veya süresi dolmuş</div>
}
// Email'i doğrula
await prisma.user.update({
where: { email: verificationToken.email },
data: { emailVerified: new Date() }
})
// Token'ı sil
await prisma.verificationToken.delete({
where: { token }
})
redirect('/login?verified=true')
}Password Reset Flow
Request Reset
// app/actions/auth.ts
'use server'
export async function requestPasswordReset(formData: FormData) {
const email = formData.get('email') as string
const user = await prisma.user.findUnique({
where: { email }
})
// Güvenlik: Kullanıcı yoksa bile aynı mesajı göster
if (!user) {
return { success: true }
}
const token = crypto.randomUUID()
const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 saat
await prisma.passwordResetToken.upsert({
where: { email },
update: { token, expires },
create: { email, token, expires }
})
await sendPasswordResetEmail(email, token)
return { success: true }
}Reset Password
// app/actions/auth.ts
'use server'
export async function resetPassword(formData: FormData) {
const token = formData.get('token') as string
const password = formData.get('password') as string
const resetToken = await prisma.passwordResetToken.findUnique({
where: { token }
})
if (!resetToken || resetToken.expires < new Date()) {
return { error: 'Token geçersiz veya süresi dolmuş' }
}
const hashedPassword = await hash(password, 12)
await prisma.user.update({
where: { email: resetToken.email },
data: { password: hashedPassword }
})
await prisma.passwordResetToken.delete({
where: { token }
})
return { success: true }
}Güvenlik Best Practices
1. Environment Variables
# Production'da güçlü secret
AUTH_SECRET="minimum-32-karakter-rastgele-string"
# HTTPS zorunlu
AUTH_URL="https://example.com"
AUTH_TRUST_HOST=true2. Cookie Settings
cookies: {
sessionToken: {
name: '__Secure-next-auth.session-token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
}
}
}3. Rate Limiting
// middleware.ts
const loginAttempts = new Map<string, { count: number; lastAttempt: number }>()
function checkLoginRateLimit(ip: string): boolean {
const now = Date.now()
const attempts = loginAttempts.get(ip)
if (!attempts || now - attempts.lastAttempt > 15 * 60 * 1000) {
loginAttempts.set(ip, { count: 1, lastAttempt: now })
return true
}
if (attempts.count >= 5) {
return false
}
attempts.count++
attempts.lastAttempt = now
return true
}4. Password Requirements
const passwordSchema = z
.string()
.min(8, 'En az 8 karakter')
.regex(/[A-Z]/, 'En az bir büyük harf')
.regex(/[a-z]/, 'En az bir küçük harf')
.regex(/[0-9]/, 'En az bir rakam')
.regex(/[^A-Za-z0-9]/, 'En az bir özel karakter')Sık Sorulan Sorular
NextAuth.js v5 ile v4 arasındaki farklar nelerdir?
V5 tamamen yeniden yazıldı. App Router native desteği, edge runtime uyumu, daha basit API. getServerSession yerine auth() kullanılıyor.
JWT mi Database session mı kullanmalıyım?
Credentials provider kullanıyorsanız JWT zorunlu. Sadece OAuth kullanıyorsanız database session'ı tercih edebilirsiniz. Session revocation gerekiyorsa database.
Refresh token nasıl implement edilir?
OAuth provider'lar için NextAuth otomatik handle eder. Credentials için custom implementation gerekir.
Multiple auth providers nasıl link edilir?
Aynı email ile farklı provider'lardan giriş yapıldığında NextAuth hesapları otomatik link eder (aynı adapter ile).
Edge runtime'da çalışır mı?
Evet, v5 edge runtime destekliyor. Ancak bazı adapter'lar (Prisma gibi) edge'de çalışmaz, bu durumda JWT strategy kullanın.
Session'ı manuel olarak nasıl sonlandırırım?
Database strategy ile session kaydını silmeniz yeterli. JWT için token'ı blacklist'e almanız gerekir.
Sonuç
NextAuth.js, Next.js uygulamaları için kapsamlı ve güvenli authentication çözümü sunuyor. OAuth entegrasyonundan email/password authentication'a, role-based access control'den email verification'a kadar tüm ihtiyaçlarınızı karşılayabilir.
Bu rehberde production-ready authentication sistemi kurmanın tüm adımlarını ele aldık. Güvenlik best practices'i uygulayarak kullanıcılarınızın verilerini koruyabilirsiniz.
Sonraki Adımlar:
- NextAuth.js'i projenize entegre edin
- En az bir OAuth provider ekleyin
- Email/password authentication implement edin
- Role-based access control kurun
- Email verification ekleyin
Serinin devamında Next.js caching stratejilerini ve ISR'ı işleyeceğiz. Performance optimizasyonlarını derinlemesine öğreneceksiniz.
Projenizi Hayata Geçirelim
Web sitesi, mobil uygulama veya yapay zeka çözümü mü arıyorsunuz? Fikirlerinizi birlikte değerlendirelim.
Ücretsiz Danışmanlık Alın