Building a Modern Authentication System in Next.js
Learn how to implement a secure, feature-rich authentication system in Next.js using NextAuth.js, including social logins, email verification, and role-based access control.
In this article:
- Why Modern Authentication Matters
- Setting Up NextAuth.js
- Implementing Authentication Features
- Security Best Practices
- Advanced Features
- Conclusion
Why Modern Authentication Matters
In today's digital landscape, a robust authentication system is crucial for:
- Protecting user data and privacy
- Providing seamless user experiences
- Supporting multiple authentication methods
- Implementing role-based access control
- Maintaining session security
Setting Up NextAuth.js
First, install the required dependencies:
npm install next-auth @prisma/client bcryptjs
npm install -D @types/bcryptjs
Create the NextAuth configuration:
// pages/api/auth/[...nextauth].ts
import { prisma } from '@/lib/prisma'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { compare } from 'bcryptjs'
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GithubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
export default NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid credentials')
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
})
if (!user || !user.password) {
throw new Error('User not found')
}
const isValid = await compare(credentials.password, user.password)
if (!isValid) {
throw new Error('Invalid password')
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
},
}),
],
session: {
strategy: 'jwt',
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
}
return token
},
async session({ session, token }) {
if (session?.user) {
session.user.role = token.role
}
return session
},
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
verifyRequest: '/auth/verify-request',
},
})
Implementing Authentication Features
1. Social Login Integration
Create a sign-in component that supports multiple providers:
// components/auth/SignInButtons.tsx
import { signIn } from 'next-auth/react'
export default function SignInButtons() {
return (
<div className="space-y-4">
<button
onClick={() => signIn('github')}
className="w-full flex items-center justify-center gap-2 bg-black text-white p-2 rounded"
>
<GithubIcon />
Continue with GitHub
</button>
<button
onClick={() => signIn('google')}
className="w-full flex items-center justify-center gap-2 bg-white text-black border p-2 rounded"
>
<GoogleIcon />
Continue with Google
</button>
</div>
)
}
2. Email Authentication
Implement email verification:
// lib/auth/email.ts
import { prisma } from '@/lib/prisma'
import { randomBytes } from 'crypto'
import { createTransport } from 'nodemailer'
export async function sendVerificationEmail(email: string) {
const token = randomBytes(32).toString('hex')
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
await prisma.verificationToken.create({
data: {
identifier: email,
token,
expires,
},
})
const transporter = createTransport({
host: process.env.EMAIL_SERVER_HOST,
port: Number(process.env.EMAIL_SERVER_PORT),
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
})
await transporter.sendMail({
to: email,
subject: 'Verify your email',
html: `
<p>Click the link below to verify your email:</p>
<a href="${process.env.NEXTAUTH_URL}/auth/verify?token=${token}">
Verify Email
</a>
`,
})
}
3. Role-Based Access Control
Create a middleware to protect routes:
// middleware.ts
import { withAuth } from 'next-auth/middleware'
import { NextResponse } from 'next/server'
export default withAuth(
function middleware(req) {
const token = req.nextauth.token
const isAdmin = token?.role === 'ADMIN'
const isApiRoute = req.nextUrl.pathname.startsWith('/api')
if (isApiRoute && !isAdmin) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
},
{
callbacks: {
authorized: ({ token }) => !!token,
},
},
)
export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
}
4. Session Management
Implement session handling:
// hooks/useSession.ts
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
export function useAuth() {
const { data: session, status } = useSession()
const router = useRouter()
const requireAuth = () => {
if (status === 'loading') return
if (!session) {
router.push('/auth/signin')
}
}
const requireRole = (role: string) => {
if (status === 'loading') return
if (!session || session.user.role !== role) {
router.push('/unauthorized')
}
}
return {
session,
status,
requireAuth,
requireRole,
}
}
Security Best Practices
- Use Environment Variables: Store sensitive data in
.env.local
- Implement Rate Limiting: Prevent brute force attacks
- Enable CSRF Protection: Use NextAuth.js built-in protection
- Set Secure Cookies: Configure proper cookie settings
- Implement Password Policies: Enforce strong passwords
- Use HTTPS: Always use secure connections
Advanced Features
- Two-Factor Authentication:
// lib/auth/2fa.ts
import { authenticator } from 'otplib'
import QRCode from 'qrcode'
export async function setup2FA(userId: string) {
const secret = authenticator.generateSecret()
const user = await prisma.user.findUnique({
where: { id: userId },
})
const otpauth = authenticator.keyuri(user.email, 'YourApp', secret)
const qrCode = await QRCode.toDataURL(otpauth)
await prisma.user.update({
where: { id: userId },
data: { twoFactorSecret: secret },
})
return { secret, qrCode }
}
- Session Management:
// lib/auth/session.ts
export async function revokeAllSessions(userId: string) {
await prisma.session.deleteMany({
where: { userId },
})
}
Conclusion
Building a modern authentication system requires careful consideration of security, user experience, and maintainability. By following this guide, you can implement a robust authentication system that supports multiple providers, role-based access control, and advanced security features.
Remember to:
- Keep dependencies updated
- Monitor security advisories
- Implement proper error handling
- Test thoroughly
- Document your authentication flow
Let me know if you need help implementing any part of the authentication system!
Thanks for reading! 🚀