How to Add Authentication to Your SaaS App with NextAuth.js?
Complete guide to adding secure authentication to your SaaS application using NextAuth.js

How to Add Authentication to Your SaaS App with NextAuth.js
Secure your app the right way — step by step.
Your App is Open to Everyone. That's a Problem.
Imagine building a SaaS app with premium features, user dashboards, and private data.
Now imagine anyone can access all of it — without logging in.
No accounts. No passwords. No protection.
That's a security nightmare. And it's more common than you think.
Authentication is the foundation of every serious web application. Get it wrong — and your entire app is exposed. Get it right — and you have a professional, secure product that users trust.
Let me show you how to get it right. With NextAuth.js.
Why NextAuth.js?
There are many authentication solutions out there. Here's why NextAuth.js is my go-to:
| Feature | NextAuth.js | Clerk | Firebase Auth |
|---|---|---|---|
| Price | Free | Paid after limit | Free tier |
| Control | Full | Limited | Limited |
| Open Source | ✅ | ❌ | ❌ |
| Next.js integration | Perfect | Good | Manual |
| Custom database | ✅ | ❌ | ❌ |
| OAuth providers | 50+ | 20+ | 10+ |
NextAuth.js gives you full control — for free. Forever.
What We're Building
By the end of this guide, you'll have:
- ✅ Google OAuth login
- ✅ Email/password login
- ✅ Protected routes — only logged in users can access
- ✅ User data saved to MongoDB
- ✅ Session management
What You'll Need
- Next.js 14 project
- MongoDB Atlas account
- Google Cloud Console account — for OAuth
- Basic JavaScript knowledge
Step 1 — Install NextAuth.js
npm install next-auth bcryptjs
npm install -D @types/bcryptjs
Add to .env.local:
NEXTAUTH_SECRET=your_random_secret_key_here
NEXTAUTH_URL=http://localhost:3000
GOOGLE_ID=your_google_client_id
GOOGLE_SECRET=your_google_client_secret
MONGODB_URI=your_mongodb_connection_string
Step 2 — Create User Model
Create /models/User.js:
import mongoose from 'mongoose'
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
// Not required — Google users won't have password
},
image: {
type: String,
},
isPro: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
}
})
export default mongoose.models.User ||
mongoose.model('User', UserSchema)
Step 3 — Setup Google OAuth
Before writing code — get your Google credentials:
1. Go to console.cloud.google.com
2. Create new project → "My SaaS App"
3. APIs & Services → Credentials
4. Create Credentials → OAuth Client ID
5. Application type → Web application
6. Authorized redirect URIs:
→ http://localhost:3000/api/auth/callback/google
7. Copy Client ID and Client Secret
8. Paste in .env.local
Step 4 — Configure NextAuth
Create /app/api/auth/[...nextauth]/route.js:
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'
import bcrypt from 'bcryptjs'
import connectDB from '@/lib/mongodb'
import User from '@/models/User'
export const authOptions = {
providers: [
// Google OAuth
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
// Email + Password
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
await connectDB()
// Find user by email
const user = await User.findOne({
email: credentials.email
})
if (!user) {
throw new Error('No user found with this email')
}
// Check password
const isValid = await bcrypt.compare(
credentials.password,
user.password
)
if (!isValid) {
throw new Error('Incorrect password')
}
return user
}
})
],
callbacks: {
// Save Google user to database
async signIn({ user, account }) {
if (account.provider === 'google') {
await connectDB()
const existingUser = await User.findOne({
email: user.email
})
if (!existingUser) {
await User.create({
name: user.name,
email: user.email,
image: user.image,
})
}
}
return true
},
// Add user data to session
async session({ session }) {
await connectDB()
const dbUser = await User.findOne({
email: session.user.email
})
session.user.id = dbUser._id.toString()
session.user.isPro = dbUser.isPro
return session
}
},
pages: {
signIn: '/login',
error: '/login',
},
secret: process.env.NEXTAUTH_SECRET,
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
Step 5 — Register New Users
Create /app/api/register/route.js:
import bcrypt from 'bcryptjs'
import connectDB from '@/lib/mongodb'
import User from '@/models/User'
export async function POST(req) {
const { name, email, password } = await req.json()
// Validate input
if (!name || !email || !password) {
return Response.json(
{ error: 'All fields are required' },
{ status: 400 }
)
}
await connectDB()
// Check if user already exists
const existingUser = await User.findOne({ email })
if (existingUser) {
return Response.json(
{ error: 'User already exists' },
{ status: 400 }
)
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12)
// Create user
await User.create({
name,
email,
password: hashedPassword,
})
return Response.json({
message: 'Account created successfully'
})
}
Step 6 — Build Login Page
Create /app/login/page.jsx:
'use client'
import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
const result = await signIn('credentials', {
email,
password,
redirect: false,
})
if (result?.error) {
setError('Invalid email or password')
setLoading(false)
return
}
router.push('/dashboard')
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white rounded-2xl p-8 shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold mb-6">Welcome back</h1>
{error && (
<div className="bg-red-50 text-red-500 p-3 rounded-lg mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full border rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
<button
type="submit"
disabled={loading}
className="w-full bg-black text-white py-3 rounded-xl font-medium hover:bg-gray-800 transition-all"
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
<div className="my-4 text-center text-gray-400">or</div>
<button
onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
className="w-full border py-3 rounded-xl font-medium hover:bg-gray-50 transition-all flex items-center justify-center gap-2"
>
Continue with Google
</button>
</div>
</div>
)
}
Step 7 — Protect Routes with Middleware
Create middleware.js in root directory:
export { default } from 'next-auth/middleware'
export const config = {
matcher: [
'/dashboard/:path*',
'/settings/:path*',
'/profile/:path*'
]
}
Done. Anyone trying to access /dashboard without logging in gets redirected to /login automatically. ✅
Step 8 — Use Session in Components
'use client'
import { useSession, signOut } from 'next-auth/react'
export default function Dashboard() {
const { data: session, status } = useSession()
if (status === 'loading') return <div>Loading...</div>
if (!session) return <div>Not logged in</div>
return (
<div>
<h1>Welcome, {session.user.name}! 👋</h1>
<p>Email: {session.user.email}</p>
<p>Plan: {session.user.isPro ? '⭐ Pro' : 'Free'}</p>
<button onClick={() => signOut({ callbackUrl: '/' })}>
Sign Out
</button>
</div>
)
}
Common Mistakes to Avoid
❌ Forgetting NEXTAUTH_SECRET — app won't work in production
❌ Not adding redirect URIs in Google Console
❌ Storing plain text passwords — always hash with bcrypt
❌ Not protecting API routes — anyone can call them
❌ Forgetting to wrap app with SessionProvider
Wrap App with SessionProvider
In /app/layout.js:
import { SessionProvider } from 'next-auth/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
)
}
The Bigger Picture
Authentication isn't just a technical feature. It's the first impression users have of your product.
A smooth, fast login experience builds trust instantly.
Slow/broken login → User leaves forever
Smooth login → User trusts your product
Authentication done right is invisible. Users don't notice it — because it just works.
What's Next?
Now that authentication is solid, here's what to add:
- 🔑 Forgot password — email reset flow
- 📱 Two-factor authentication — extra security
- 🎭 Role-based access — admin vs user
- 📊 Login analytics — track user activity
- 🔒 Rate limiting — prevent brute force attacks
Have questions about NextAuth.js? Drop a comment below — I read every single one.




