Skip to main content

Command Palette

Search for a command to run...

How to Add Authentication to Your SaaS App with NextAuth.js?

Complete guide to adding secure authentication to your SaaS application using NextAuth.js

Updated
7 min read
How to Add Authentication to Your SaaS App with NextAuth.js?
N
Hi there! I'm a Full-Stack Developer and AI Automation Engineer crafting innovative web applications and automation workflows that saves time, reduces effort, scale your growth, and drive real results.

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.