Beginner's Guide to Next.js Authentication with NextAuth.js and MongoDB (2025 Edition)
Introduction: What is Authentication and Why Do You Need It?
Ever visited a website that remembers who you are and shows your personal information? That's authentication in action! Authentication is like the digital version of checking someone's ID - it verifies that users are who they claim to be.
Adding authentication to your Next.js website is important because it:
- Keeps user information safe from unauthorized access
- Allows you to create personalized experiences for each user
- Protects private sections of your website
- Builds trust with your visitors who know their data is secure
- Helps comply with data protection regulations
NextAuth.js makes adding authentication to your Next.js app super easy, even if you're a beginner. It works with popular login methods like Google and GitHub, and you can also use traditional email/password login. Best of all, we'll connect it to MongoDB, a beginner-friendly database that's perfect for web applications!
Authentication Terminology for Beginners
Before diving into code, let's understand some key terms:
Authentication vs. Authorization:
- Authentication: Verifying who a user is (login)
- Authorization: Determining what a user can access (permissions)
OAuth: A standard protocol that allows users to log in to your app using accounts from services like Google or GitHub without sharing their passwords with you.
JWT (JSON Web Tokens): Small, secure tokens that contain user information. Think of them like digital wristbands at an event that show you've already checked in.
Session: A way to remember a user between page visits. Like a conversation that continues even if you pause and come back later.
Cookies: Small pieces of data stored in a user's browser that help maintain their login state.
Hashing: A one-way process to securely store passwords by converting them to scrambled text that can't be reversed.
What You'll Build in This Tutorial
By the end of this beginner-friendly guide, you'll create a Next.js application with:
- A login page with social login buttons (Google and GitHub)
- Secure email/password login that properly protects passwords
- Protected pages that only logged-in users can access
- User profiles showing information from their social accounts
- Data storage in MongoDB, a popular and free database
- Session management that keeps users logged in
![Authentication Flow Example]
What You'll Need Before Starting
- Basic knowledge of JavaScript and React (you don't need to be an expert!)
- Node.js installed on your computer (download from nodejs.org)
- A code editor like Visual Studio Code (it's free!)
- A free MongoDB Atlas account (we'll show you how to set this up)
- A free GitHub or Google account for setting up social login
Don't worry if you're new to some of these technologies - we'll explain each step clearly!
Step 1: Understanding How NextAuth.js Works
Before writing any code, let's understand how NextAuth.js handles authentication:
- User Starts Login: User clicks "Sign in with Google" or enters email/password
- Authentication Request: NextAuth redirects to the provider (Google, GitHub) or checks credentials
- Provider Response: After authentication, the provider sends information back to NextAuth
- Session Creation: NextAuth creates a secure session and JWT (JSON Web Token)
- User is Logged In: NextAuth sets cookies to maintain the session
The beauty of NextAuth is that it handles all these steps for you - you just need to set up the configuration!
Authentication Flow Types
NextAuth supports two main ways to handle sessions:
JWT Strategy (default):
- Stores user information in an encrypted token in the browser
- Works without a database (great for simple projects)
- Fast and stateless (no server lookups needed)
Database Strategy:
- Stores sessions in your MongoDB database
- Gives you more control over user sessions
- Better for more complex applications
We'll use the JWT strategy for simplicity but explain how to switch if needed.
Step 2: Creating Your Next.js Project
Let's start by creating a new Next.js application. Open your terminal (Command Prompt or Terminal app) and run:
Bash1npx create-next-app@latest my-auth-app
2cd my-auth-app
This command creates a new Next.js project with the latest features. When prompted, select the default options.
Tip: If you're completely new to Next.js, take a few minutes to explore the generated files. The main folders are:
/app
: Contains your pages and components/public
: For static files like images- Next.js 13+ uses the App Router, which might look different from older tutorials!
Step 3: Installing NextAuth.js
Now let's add the authentication library. In your terminal, run:
Bashnpm install next-auth@latest @types/next-auth
This installs NextAuth.js and its TypeScript types. NextAuth will handle all the complex authentication logic for us!
Step 4: Understanding Environment Variables and Security
When building apps with authentication, keeping secrets secure is critical. Let's understand why:
What are environment variables? Environment variables are like secret notes your application can read but aren't visible in your code. They're perfect for storing sensitive information like API keys.
Why use them?
- They keep sensitive data out of your code repository
- They let you use different values in development and production
- They're a standard practice for security
Let's set up your environment variables:
- Create a file named
.env.local
in your project's main folder - Add the following template:
Plain Text1NEXTAUTH_URL=http://localhost:3000 2NEXTAUTH_SECRET=your_secure_key_here 3MONGODB_URI=your_mongodb_connection_string 4GOOGLE_CLIENT_ID=your_google_id 5GOOGLE_CLIENT_SECRET=your_google_secret 6GITHUB_CLIENT_ID=your_github_id 7GITHUB_CLIENT_SECRET=your_github_secret
The NEXTAUTH_SECRET
is particularly important - it's used to encrypt your tokens. Generate a secure random string by:
On Mac/Linux: Open Terminal and type:
Bashopenssl rand -base64 32
On Windows: Open PowerShell and type:
Powershell[Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
Security Tip: Never commit your
.env.local
file to Git! Add it to your.gitignore
file to prevent accidentally sharing your secrets.
Step 5: Understanding OAuth and Setting Up Providers
What is OAuth and Why Use It?
OAuth allows users to log in using their existing accounts from services like Google or GitHub. Benefits include:
- Users don't need to create yet another password
- You don't have to handle password security
- Higher trust as users authenticate with brands they recognize
- Access to profile information from these providers
Now let's set up two popular OAuth providers:
Setting Up Google Authentication
- Go to the Google Cloud Console
- Create a new project by clicking the project dropdown at the top and selecting "New Project"
- Give your project a name like "My Auth App" and click "Create"
- Once created, select it and go to "APIs & Services" > "Credentials"
- Click "Configure Consent Screen" (select "External" if prompted)
- Fill in required information (app name, support email)
- Add your email as a test user and complete the form
- Return to Credentials, click "Create Credentials" > "OAuth client ID"
- Select "Web Application" as the application type
- Add
http://localhost:3000
under "Authorized JavaScript origins" - Add
http://localhost:3000/api/auth/callback/google
under "Authorized redirect URIs" - Copy the provided Client ID and Client Secret to your
.env.local
file
Tip: The redirect URI is where Google will send users after they log in. This must match exactly what NextAuth expects.
Setting Up GitHub Authentication
- Go to GitHub Developer Settings
- Click "New OAuth App"
- Fill in:
- Application name: "My Next.js Auth App"
- Homepage URL:
http://localhost:3000
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Copy the Client ID and generate a Client Secret
- Add these to your
.env.local
file
Step 6: Understanding NoSQL Databases and MongoDB
What is MongoDB and Why Use It?
MongoDB is a "NoSQL" database that stores data in flexible, JSON-like documents. It's different from traditional SQL databases in several ways:
- Flexible Schema: You can add fields to documents without redefining the entire database structure
- JSON-like Documents: Data is stored in a format that's natural for JavaScript developers
- Easy Scaling: Great for applications that might grow quickly
- Perfect for User Data: Excellent for storing user profiles with varying information
For our authentication system, MongoDB will store:
- User account information
- Provider connections (links to Google/GitHub accounts)
- Sessions (if you choose database sessions instead of JWT)
Setting Up MongoDB Atlas
MongoDB Atlas provides a free cloud database perfect for learning and small projects:
- Go to MongoDB Atlas and create a free account
- After signing up, create a new cluster (select the FREE tier)
- Choose a cloud provider and region closest to you
- Click "Create Cluster" (this takes a few minutes)
- Once ready, click "Connect"
- Create a database user with a secure password
- Under "Where would you like to connect from?" add
0.0.0.0/0
to allow access from anywhere - Choose "Connect your application" and copy the connection string
- Replace
<password>
with your database user's password - Add the connection string to your
.env.local
file asMONGODB_URI
Understanding Mongoose
Mongoose is a library that makes working with MongoDB easier in Node.js:
- It provides a schema-based solution to model your data
- It offers built-in type casting and validation
- It handles the connection to MongoDB for you
Install MongoDB packages:
Bashnpm install mongodb mongoose @auth/mongodb-adapter
Step 7: Understanding Models in Mongoose
Before writing code, let's understand what database models are:
Models are like blueprints for your data. They define:
- What fields your data has (name, email, password)
- What types these fields should be (string, number, date)
- Which fields are required and which are optional
- Any validation rules (minimum length, format)
For our authentication system, we need models for:
- User: Stores basic user information (name, email, image)
- Account: Links users to their social providers
- Session: Stores active login sessions
- Verification Token: For email verification
Here's a simplified look at our User model:
JavaScript1// This defines what user data looks like in our database
2const UserSchema = new mongoose.Schema({
3 name: String,
4 email: {
5 type: String,
6 unique: true // No duplicate emails allowed
7 },
8 password: String,
9 image: String
10});
We'll implement these models in the following sections.
Step 8: Setting Up Database Connection
Now let's create the files to connect to MongoDB:
- Create a
lib
folder in your project root - Create
lib/mongodb.js
file:
JavaScript1import mongoose from 'mongoose';
2
3const MONGODB_URI = process.env.MONGODB_URI;
4
5if (!MONGODB_URI) {
6 throw new Error('Please define the MONGODB_URI environment variable');
7}
8
9// This prevents multiple connections during development
10let cached = global.mongoose;
11
12if (!cached) {
13 cached = global.mongoose = { conn: null, promise: null };
14}
15
16async function dbConnect() {
17 if (cached.conn) {
18 return cached.conn;
19 }
20
21 if (!cached.promise) {
22 cached.promise = mongoose.connect(MONGODB_URI).then(mongoose => {
23 return mongoose;
24 });
25 }
26
27 cached.conn = await cached.promise;
28 return cached.conn;
29}
30
31export default dbConnect;
This code establishes a connection to MongoDB and caches it to prevent creating multiple connections.
- Create a separate client for the NextAuth adapter in
lib/mongodb-client.js
:
JavaScript1import { MongoClient } from 'mongodb';
2
3const uri = process.env.MONGODB_URI;
4const options = {};
5
6let client;
7let clientPromise;
8
9if (!process.env.MONGODB_URI) {
10 throw new Error('Please add your MongoDB URI to .env.local');
11}
12
13// In development, use a global variable to preserve the connection across hot reloads
14if (process.env.NODE_ENV === 'development') {
15 if (!global._mongoClientPromise) {
16 client = new MongoClient(uri, options);
17 global._mongoClientPromise = client.connect();
18 }
19 clientPromise = global._mongoClientPromise;
20} else {
21 // In production, create a new client
22 client = new MongoClient(uri, options);
23 clientPromise = client.connect();
24}
25
26export default clientPromise;
Tip for Beginners: The difference between these two files might be confusing. The first uses Mongoose (a higher-level library) for our custom code, while the second uses the raw MongoDB driver for NextAuth's adapter.
Step 9: Creating User Models
Now let's create our database models. Create a models
folder and add these files:
models/User.js
:
JavaScript1import mongoose from 'mongoose';
2
3// Only create the model if it doesn't exist already
4const UserSchema = new mongoose.Schema({
5 name: String,
6 email: {
7 type: String,
8 unique: true,
9 sparse: true,
10 },
11 emailVerified: Date,
12 image: String,
13 password: String,
14});
15
16export default mongoose.models.User || mongoose.model('User', UserSchema);
models/Account.js
:
JavaScript1import mongoose from 'mongoose';
2
3const AccountSchema = new mongoose.Schema({
4 userId: {
5 type: mongoose.Schema.Types.ObjectId,
6 ref: 'User',
7 },
8 type: String,
9 provider: String,
10 providerAccountId: String,
11 refresh_token: String,
12 access_token: String,
13 expires_at: Number,
14 token_type: String,
15 scope: String,
16 id_token: String,
17 session_state: String,
18});
19
20// Make sure we don't connect the same account twice
21AccountSchema.index({ provider: 1, providerAccountId: 1 }, { unique: true });
22
23export default mongoose.models.Account || mongoose.model('Account', AccountSchema);
- Create similar models for
Session.js
andVerificationToken.js
Understanding Mongoose Models: These models define the structure of our data. The
mongoose.models.X || mongoose.model('X', Schema)
pattern prevents errors when the app hot reloads during development.
Step 10: Understanding JWT vs. Database Sessions
Before configuring NextAuth, let's understand the two session strategies:
JWT Sessions (Default)
- How it works: User info is stored in an encrypted token in the browser
- Pros: Faster (no database lookups), works without a database, simpler
- Cons: Can't revoke sessions immediately, limited storage
Database Sessions
- How it works: Only a session ID is stored in the browser; all data is in the database
- Pros: Can revoke sessions instantly, can store more user data
- Cons: Requires database lookups on each request, slightly more complex
For beginners, JWT sessions are usually simpler, but knowing the difference helps you make an informed choice.
Step 11: Configuring NextAuth API Route
Now let's set up the core of our authentication system. Create a file at app/api/auth/[...nextauth]/route.js
:
JavaScript1import NextAuth from "next-auth";
2import GoogleProvider from "next-auth/providers/google";
3import GitHubProvider from "next-auth/providers/github";
4import CredentialsProvider from "next-auth/providers/credentials";
5import { MongoDBAdapter } from "@auth/mongodb-adapter";
6import { compare } from "bcryptjs";
7import clientPromise from "@/lib/mongodb-client";
8import User from "@/models/User";
9import dbConnect from "@/lib/mongodb";
10
11// This is where we configure our authentication system
12export const authOptions = {
13 // Use MongoDB adapter to store user data
14 adapter: MongoDBAdapter(clientPromise),
15
16 // Configure authentication providers
17 providers: [
18 // Google login
19 GoogleProvider({
20 clientId: process.env.GOOGLE_CLIENT_ID,
21 clientSecret: process.env.GOOGLE_CLIENT_SECRET,
22 }),
23
24 // GitHub login
25 GitHubProvider({
26 clientId: process.env.GITHUB_CLIENT_ID,
27 clientSecret: process.env.GITHUB_CLIENT_SECRET,
28 }),
29
30 // Email/password login
31 CredentialsProvider({
32 name: "Email",
33 credentials: {
34 email: { label: "Email", type: "email" },
35 password: { label: "Password", type: "password" }
36 },
37 async authorize(credentials) {
38 if (!credentials?.email || !credentials?.password) {
39 return null;
40 }
41
42 // Connect to database
43 await dbConnect();
44
45 // Find user by email
46 const user = await User.findOne({ email: credentials.email });
47
48 if (!user || !user.password) {
49 return null;
50 }
51
52 // Check if password matches
53 const isPasswordValid = await compare(
54 credentials.password,
55 user.password
56 );
57
58 if (!isPasswordValid) {
59 return null;
60 }
61
62 // Return user object if authentication succeeds
63 return {
64 id: user._id.toString(),
65 email: user.email,
66 name: user.name,
67 image: user.image,
68 };
69 }
70 }),
71 ],
72
73 // Session configuration
74 session: {
75 strategy: "jwt", // Use JWT for sessions
76 maxAge: 30 * 24 * 60 * 60, // 30 days
77 },
78
79 // Callbacks to customize behavior
80 callbacks: {
81 async session({ session, token }) {
82 if (token && session.user) {
83 session.user.id = token.sub;
84 // You can add custom fields here
85 }
86 return session;
87 },
88 },
89
90 // Pages for custom authentication flow
91 pages: {
92 signIn: "/auth/signin",
93 error: "/auth/error",
94 },
95};
96
97// This creates the API route handler
98const handler = NextAuth(authOptions);
99export { handler as GET, handler as POST };
Understanding This Code: This is the heart of your authentication system. It:
- Sets up providers (Google, GitHub, email/password)
- Configures session handling
- Defines callback functions for customizing behavior
- Specifies custom pages for the authentication flow
Step 12: Setting Up the Session Provider
For authentication to work across your app, you need to wrap it with a session provider. Create app/providers.jsx
:
React JSX1"use client";
2
3import { SessionProvider } from "next-auth/react";
4
5export function Providers({ children }) {
6 return <SessionProvider>{children}</SessionProvider>;
7}
Then update your app/layout.jsx
:
React JSX1import { Providers } from "./providers";
2import "./globals.css";
3
4export const metadata = {
5 title: "My Auth App",
6 description: "A Next.js app with authentication",
7};
8
9export default function RootLayout({ children }) {
10 return (
11 <html lang="en">
12 <body>
13 <Providers>{children}</Providers>
14 </body>
15 </html>
16 );
17}
This ensures the authentication state is available throughout your application.
Tip: The "use client" directive is important here because SessionProvider uses browser APIs that aren't available during server rendering.
Step 13: Creating a Sign-In Page
Let's create a simple but attractive sign-in page. Create a file at app/auth/signin/page.jsx
:
React JSX1"use client";
2
3import { useState, useEffect } from "react";
4import { signIn, getProviders } from "next-auth/react";
5import { useRouter } from "next/navigation";
6
7export default function SignIn() {
8 const [email, setEmail] = useState("");
9 const [password, setPassword] = useState("");
10 const [error, setError] = useState("");
11 const [providers, setProviders] = useState(null);
12 const router = useRouter();
13
14 // Load available authentication providers
15 useEffect(() => {
16 const loadProviders = async () => {
17 const providers = await getProviders();
18 setProviders(providers);
19 };
20 loadProviders();
21 }, []);
22
23 // Handle email/password login
24 const handleEmailSignIn = async (e) => {
25 e.preventDefault();
26 setError("");
27
28 try {
29 const result = await signIn("credentials", {
30 redirect: false,
31 email,
32 password,
33 });
34
35 if (result?.error) {
36 setError("Invalid email or password");
37 } else {
38 router.push("/dashboard");
39 }
40 } catch (error) {
41 setError("An unexpected error occurred");
42 }
43 };
44
45 // If providers haven't loaded yet
46 if (!providers) {
47 return <div>Loading...</div>;
48 }
49
50 return (
51 <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
52 <div className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg">
53 <div className="text-center">
54 <h1 className="text-3xl font-bold text-gray-900">Sign In</h1>
55 <p className="mt-2 text-gray-600">Access your account</p>
56 </div>
57
58 {error && (
59 <div className="bg-red-50 text-red-500 p-3 rounded-lg text-sm">
60 {error}
61 </div>
62 )}
63
64 {/* Email/Password Form */}
65 <form className="mt-8 space-y-6" onSubmit={handleEmailSignIn}>
66 <div>
67 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
68 Email address
69 </label>
70 <input
71 id="email"
72 name="email"
73 type="email"
74 required
75 value={email}
76 onChange={(e) => setEmail(e.target.value)}
77 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
78 />
79 </div>
80
81 <div>
82 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
83 Password
84 </label>
85 <input
86 id="password"
87 name="password"
88 type="password"
89 required
90 value={password}
91 onChange={(e) => setPassword(e.target.value)}
92 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
93 />
94 </div>
95
96 <button
97 type="submit"
98 className="w-full py-2 px-4 border border-transparent rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
99 >
100 Sign in with Email
101 </button>
102 </form>
103
104 {/* Divider */}
105 <div className="mt-6">
106 <div className="relative">
107 <div className="absolute inset-0 flex items-center">
108 <div className="w-full border-t border-gray-300" />
109 </div>
110 <div className="relative flex justify-center text-sm">
111 <span className="px-2 bg-white text-gray-500">Or continue with</span>
112 </div>
113 </div>
114
115 {/* Social Login Buttons */}
116 <div className="mt-6 grid grid-cols-2 gap-3">
117 {providers &&
118 Object.values(providers)
119 .filter((provider) => provider.id !== "credentials")
120 .map((provider) => (
121 <button
122 key={provider.id}
123 onClick={() => signIn(provider.id, { callbackUrl: "/dashboard" })}
124 className="py-2 px-4 border border-gray-300 rounded-md bg-white hover:bg-gray-50"
125 >
126 Sign in with {provider.name}
127 </button>
128 ))}
129 </div>
130 </div>
131 </div>
132 </div>
133 );
134}
This page provides both email/password login and social login buttons.
UI Tip: This is a basic design using Tailwind CSS (included with Next.js by default). Feel free to customize the appearance to match your app's style!
Step 14: Understanding Route Protection
One of the main reasons for adding authentication is to protect certain routes from unauthorized access. Let's understand how this works in Next.js with the App Router:
Server-Side Protection: This checks if a user is logged in before rendering a page. If not, they are redirected to the login page.
Client-Side Protection: This hides or shows components based on the user's login status.
Let's implement both approaches:
Server-Side Protection
Create a dashboard page at app/dashboard/page.jsx
:
React JSX1import { getServerSession } from "next-auth";
2import { authOptions } from "@/app/api/auth/[...nextauth]/route";
3import { redirect } from "next/navigation";
4
5export default async function Dashboard() {
6 // Get the session on the server
7 const session = await getServerSession(authOptions);
8
9 // If no session exists, redirect to login
10 if (!session) {
11 redirect("/auth/signin");
12 }
13
14 // If we get here, the user is logged in
15 return (
16 <div className="max-w-4xl mx-auto p-6">
17 <h1 className="text-3xl font-bold mb-6">Welcome to your Dashboard</h1>
18 <div className="bg-white shadow rounded-lg p-6">
19 <p>Hello, {session.user?.name || session.user?.email}!</p>
20 <p>This page is protected and only visible if you're logged in.</p>
21 </div>
22 </div>
23 );
24}
Client-Side Protection with useSession
Create a components
folder in your project root, then add components/UserProfile.jsx
:
React JSX1"use client";
2
3import { useSession, signOut } from "next-auth/react";
4
5export default function UserProfile() {
6 // Get session on the client side
7 const { data: session, status } = useSession();
8
9 // Show loading state
10 if (status === "loading") {
11 return <div>Loading user profile...</div>;
12 }
13
14 // Show login prompt if not logged in
15 if (!session) {
16 return <p>Please sign in to view your profile</p>;
17 }
18
19 // Show user info if logged in
20 return (
21 <div>
22 <div className="flex items-center space-x-4">
23 {session.user?.image && (
24 <img
25 src={session.user.image}
26 alt="Profile picture"
27 className="w-16 h-16 rounded-full"
28 />
29 )}
30
31 <div>
32 {session.user?.name && (
33 <h2 className="text-xl font-semibold">{session.user.name}</h2>
34 )}
35 {session.user?.email && (
36 <p>{session.user.email}</p>
37 )}
38 <button
39 onClick={() => signOut({ callbackUrl: "/" })}
40 className="mt-2 text-sm text-red-600 hover:text-red-800"
41 >
42 Sign out
43 </button>
44 </div>
45 </div>
46 </div>
47 );
48}
Add this component to your dashboard:
React JSX1import { getServerSession } from "next-auth";
2import { authOptions } from "@/app/api/auth/[...nextauth]/route";
3import { redirect } from "next/navigation";
4import UserProfile from "@/components/UserProfile";
5
6export default async function Dashboard() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 redirect("/auth/signin");
11 }
12
13 return (
14 <div className="max-w-4xl mx-auto p-6">
15 <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
16 <div className="bg-white shadow rounded-lg p-6">
17 <UserProfile />
18
19 <div className="mt-8">
20 <h2 className="text-xl font-semibold mb-4">Your Secure Content</h2>
21 <p>This content is only visible to authenticated users.</p>
22 </div>
23 </div>
24 </div>
25 );
26}
Understanding the Difference: Server components check authentication during page generation, while client components can respond to login state changes in real-time.
Step 15: Adding Password Registration and Hashing
For email/password login to work, we need to add user registration and secure password storage. First, let's understand password hashing:
What is Password Hashing?
Hashing converts passwords into scrambled strings that can't be reversed:
- Original password: "mySecurePassword123"
- Hashed password: "$2a$10$XJFyHN5xnfDUVVB4T7OXA.nVgFDNUy8.YBQoQoHn8NruXGxnAYnW6"
If your database is ever compromised, attackers only see the hashed versions, not actual passwords.
Install bcryptjs for password hashing:
Bashnpm install bcryptjs
Now create a registration page at app/auth/register/page.jsx
:
React JSX1"use client";
2
3import { useState } from "react";
4import { useRouter } from "next/navigation";
5import Link from "next/link";
6
7export default function Register() {
8 const [name, setName] = useState("");
9 const [email, setEmail] = useState("");
10 const [password, setPassword] = useState("");
11 const [error, setError] = useState("");
12 const router = useRouter();
13
14 const handleSubmit = async (e) => {
15 e.preventDefault();
16 setError("");
17
18 try {
19 // Send registration data to our API
20 const response = await fetch("/api/register", {
21 method: "POST",
22 headers: { "Content-Type": "application/json" },
23 body: JSON.stringify({ name, email, password }),
24 });
25
26 const data = await response.json();
27
28 if (!response.ok) {
29 throw new Error(data.message || "Registration failed");
30 }
31
32 // Redirect to sign-in page after successful registration
33 router.push("/auth/signin?registered=true");
34 } catch (error) {
35 setError(error.message);
36 }
37 };
38
39 return (
40 <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
41 <div className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg">
42 <div className="text-center">
43 <h1 className="text-3xl font-bold text-gray-900">Create Account</h1>
44 <p className="mt-2 text-gray-600">Join our community</p>
45 </div>
46
47 {error && (
48 <div className="bg-red-50 text-red-500 p-3 rounded-lg text-sm">
49 {error}
50 </div>
51 )}
52
53 <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
54 <div>
55 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
56 Name
57 </label>
58 <input
59 id="name"
60 name="name"
61 type="text"
62 required
63 value={name}
64 onChange={(e) => setName(e.target.value)}
65 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
66 />
67 </div>
68
69 <div>
70 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
71 Email address
72 </label>
73 <input
74 id="email"
75 name="email"
76 type="email"
77 required
78 value={email}
79 onChange={(e) => setEmail(e.target.value)}
80 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
81 />
82 </div>
83
84 <div>
85 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
86 Password
87 </label>
88 <input
89 id="password"
90 name="password"
91 type="password"
92 required
93 value={password}
94 onChange={(e) => setPassword(e.target.value)}
95 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
96 />
97 </div>
98
99 <button
100 type="submit"
101 className="w-full py-2 px-4 border border-transparent rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
102 >
103 Create Account
104 </button>
105 </form>
106
107 <div className="text-center mt-4">
108 <p className="text-sm text-gray-600">
109 Already have an account?{" "}
110 <Link href="/auth/signin" className="text-indigo-600 hover:text-indigo-500">
111 Sign in
112 </Link>
113 </p>
114 </div>
115 </div>
116 </div>
117 );
118}
Now let's create the API endpoint to handle registration. Create a file at app/api/register/route.js
:
JavaScript1import { hash } from "bcryptjs";
2import User from "@/models/User";
3import dbConnect from "@/lib/mongodb";
4import { NextResponse } from "next/server";
5
6export async function POST(request) {
7 try {
8 const { name, email, password } = await request.json();
9
10 // Validate the inputs
11 if (!email || !email.includes('@') || !password || password.length < 8) {
12 return NextResponse.json(
13 { message: "Invalid input data" },
14 { status: 400 }
15 );
16 }
17
18 // Connect to the database
19 await dbConnect();
20
21 // Check if user already exists
22 const existingUser = await User.findOne({ email });
23 if (existingUser) {
24 return NextResponse.json(
25 { message: "User already exists" },
26 { status: 409 }
27 );
28 }
29
30 // Hash the password (never store plain text passwords!)
31 const hashedPassword = await hash(password, 12);
32
33 // Create the new user
34 const user = await User.create({
35 name,
36 email,
37 password: hashedPassword,
38 });
39
40 // Remove password from the response
41 const newUser = {
42 id: user._id.toString(),
43 name: user.name,
44 email: user.email,
45 };
46
47 return NextResponse.json(
48 { message: "User created successfully", user: newUser },
49 { status: 201 }
50 );
51 } catch (error) {
52 console.error("Registration error:", error);
53 return NextResponse.json(
54 { message: "An error occurred during registration" },
55 { status: 500 }
56 );
57 }
58}
Security Tips:
- Always hash passwords before storing them
- Validate user inputs to prevent malicious data
- Never return sensitive information like password hashes
- Handle errors gracefully without exposing system details
Step 16: Understanding Middleware for Multiple Protected Routes
If you have many pages that should be protected, creating server-side checks in each file becomes repetitive. Next.js provides a solution called "middleware" that can protect multiple routes at once.
What is Middleware?
Middleware runs before a request is completed, allowing you to:
- Check if a user is logged in
- Redirect unauthenticated users
- Modify request or response headers
- Apply protection rules across multiple pages
Here's how to set up route protection middleware:
Create a file named middleware.js
in your project root:
JavaScript1import { NextResponse } from "next/server";
2import { getToken } from "next-auth/jwt";
3
4export async function middleware(request) {
5 const { pathname } = request.nextUrl;
6
7 // Define which paths should be protected
8 const protectedPaths = ["/dashboard", "/profile", "/settings"];
9 const isPathProtected = protectedPaths.some((path) =>
10 pathname.startsWith(path)
11 );
12
13 // Allow public paths
14 if (!isPathProtected) {
15 return NextResponse.next();
16 }
17
18 // Check for authentication token
19 const token = await getToken({
20 req: request,
21 secret: process.env.NEXTAUTH_SECRET,
22 });
23
24 // Redirect to login if no token found
25 if (!token) {
26 const url = new URL(`/auth/signin`, request.url);
27 url.searchParams.set("callbackUrl", pathname);
28 return NextResponse.redirect(url);
29 }
30
31 // Allow access if authenticated
32 return NextResponse.next();
33}
34
35// Configure which routes use this middleware
36export const config = {
37 matcher: ["/dashboard/:path*", "/profile/:path*", "/settings/:path*"],
38};
Understanding Middleware: Think of middleware as a security guard checking badges before allowing entry to different areas of a building. The guard stands at the entrance and checks everyone before they're allowed to proceed.
Step 17: Creating a User-Friendly Home Page
Let's create a welcoming home page that adapts based on whether the user is signed in.
Create or update app/page.jsx
:
React JSX1import { getServerSession } from "next-auth/auth";
2import { authOptions } from "./api/auth/[...nextauth]/route";
3import Link from "next/link";
4
5export default async function Home() {
6 // Check if user is logged in
7 const session = await getServerSession(authOptions);
8
9 return (
10 <div className="bg-white">
11 <div className="relative isolate px-6 pt-14 lg:px-8">
12 <div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
13 <div className="text-center">
14 <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
15 Welcome to My Authentication Demo
16 </h1>
17 <p className="mt-6 text-lg leading-8 text-gray-600">
18 A simple example of Next.js authentication with NextAuth.js and MongoDB
19 </p>
20 <div className="mt-10 flex items-center justify-center gap-x-6">
21 {session ? (
22 // Show these buttons if logged in
23 <>
24 <Link
25 href="/dashboard"
26 className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white"
27 >
28 Go to Dashboard
29 </Link>
30 <Link
31 href="/profile"
32 className="text-sm font-semibold leading-6 text-gray-900"
33 >
34 View Profile <span aria-hidden="true">→</span>
35 </Link>
36 </>
37 ) : (
38 // Show these buttons if logged out
39 <>
40 <Link
41 href="/auth/signin"
42 className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white"
43 >
44 Sign In
45 </Link>
46 <Link
47 href="/auth/register"
48 className="text-sm font-semibold leading-6 text-gray-900"
49 >
50 Create Account <span aria-hidden="true">→</span>
51 </Link>
52 </>
53 )}
54 </div>
55 </div>
56 </div>
57 </div>
58 </div>
59 );
60}
Step 18: Creating a Navbar Component
A navigation bar helps users move around your app and shows their login status. Create components/Navbar.jsx
:
React JSX1"use client";
2
3import Link from "next/link";
4import { useSession, signOut } from "next-auth/react";
5import { useState } from "react";
6
7export default function Navbar() {
8 const { data: session, status } = useSession();
9 const [isMenuOpen, setIsMenuOpen] = useState(false);
10
11 return (
12 <nav className="bg-white shadow">
13 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
14 <div className="flex justify-between h-16">
15 <div className="flex">
16 <Link href="/" className="flex-shrink-0 flex items-center">
17 <span className="text-xl font-bold text-indigo-600">AuthApp</span>
18 </Link>
19 <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
20 <Link href="/" className="px-3 py-2 text-sm font-medium text-gray-900">
21 Home
22 </Link>
23 {session && (
24 <Link href="/dashboard" className="px-3 py-2 text-sm font-medium text-gray-900">
25 Dashboard
26 </Link>
27 )}
28 </div>
29 </div>
30
31 <div className="hidden sm:ml-6 sm:flex sm:items-center">
32 {status === "loading" ? (
33 <div className="text-sm text-gray-500">Loading...</div>
34 ) : session ? (
35 <div className="flex items-center space-x-4">
36 {session.user?.image && (
37 <img
38 className="h-8 w-8 rounded-full"
39 src={session.user.image}
40 alt={session.user.name || "User"}
41 />
42 )}
43 <span className="text-sm text-gray-900">{session.user?.name || session.user?.email}</span>
44 <button
45 onClick={() => signOut()}
46 className="text-sm text-red-600 hover:text-red-800"
47 >
48 Sign out
49 </button>
50 </div>
51 ) : (
52 <div className="space-x-4">
53 <Link href="/auth/signin" className="text-sm text-gray-900 hover:text-gray-600">
54 Sign in
55 </Link>
56 <Link
57 href="/auth/register"
58 className="ml-2 px-3 py-2 rounded-md text-sm text-white bg-indigo-600 hover:bg-indigo-700"
59 >
60 Register
61 </Link>
62 </div>
63 )}
64 </div>
65
66 {/* Mobile menu button */}
67 <div className="flex items-center sm:hidden">
68 <button
69 onClick={() => setIsMenuOpen(!isMenuOpen)}
70 className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
71 >
72 <span className="sr-only">Open main menu</span>
73 {isMenuOpen ? "✕" : "☰"}
74 </button>
75 </div>
76 </div>
77 </div>
78
79 {/* Mobile menu */}
80 {isMenuOpen && (
81 <div className="sm:hidden">
82 <div className="pt-2 pb-3 space-y-1">
83 <Link href="/" className="block px-3 py-2 text-base font-medium text-gray-900">
84 Home
85 </Link>
86 {session && (
87 <Link href="/dashboard" className="block px-3 py-2 text-base font-medium text-gray-900">
88 Dashboard
89 </Link>
90 )}
91 </div>
92 <div className="pt-4 pb-3 border-t border-gray-200">
93 {session ? (
94 <div className="flex items-center px-4">
95 {session.user?.image && (
96 <img
97 className="h-10 w-10 rounded-full"
98 src={session.user.image}
99 alt=""
100 />
101 )}
102 <div className="ml-3">
103 <div className="text-base font-medium text-gray-800">
104 {session.user?.name || session.user?.email}
105 </div>
106 <button
107 onClick={() => signOut()}
108 className="mt-1 text-sm text-red-600 hover:text-red-800"
109 >
110 Sign out
111 </button>
112 </div>
113 </div>
114 ) : (
115 <div className="flex flex-col space-y-2 px-4">
116 <Link href="/auth/signin" className="text-base font-medium text-gray-900">
117 Sign in
118 </Link>
119 <Link href="/auth/register" className="text-base font-medium text-gray-900">
120 Register
121 </Link>
122 </div>
123 )}
124 </div>
125 </div>
126 )}
127 </nav>
128 );
129}
Add this navbar to your layout:
React JSX1import { Providers } from "./providers";
2import Navbar from "@/components/Navbar";
3import "./globals.css";
4
5export const metadata = {
6 title: "My Auth App",
7 description: "A Next.js app with authentication",
8};
9
10export default function RootLayout({ children }) {
11 return (
12 <html lang="en">
13 <body>
14 <Providers>
15 <Navbar />
16 <main>{children}</main>
17 </Providers>
18 </body>
19 </html>
20 );
21}
Design Tip: This navbar is responsive, working well on both mobile and desktop screens. It automatically shows/hides buttons based on login status.
Step 19: Common Authentication Problems and Solutions
Even when following a guide step-by-step, you might encounter some challenges. Here are solutions to the most common issues:
"My Social Login Doesn't Work"
Problem: Clicking Google or GitHub login buttons doesn't do anything or shows errors.
Solutions:
- Double-check your environment variables (GOOGLECLIENTID, etc.) for typos
- Verify that the callback URLs in your provider settings match exactly what NextAuth expects
- Make sure you've set the NEXTAUTH_URL correctly
- Check browser console (F12) for error messages
Tip: For Google, make sure you've completed the OAuth consent screen setup and added your email as a test user if you're in testing mode.
"Users Stay Logged In Forever" or "Users Get Logged Out Too Quickly"
Problem: Session duration is too long or too short.
Solution: Adjust the maxAge property in your NextAuth config:
JavaScript1session: {
2 strategy: "jwt",
3 maxAge: 24 * 60 * 60, // 1 day in seconds (adjust as needed)
4},
"I Can't Get User Data in My Components"
Problem: You're trying to access session data but it's undefined.
Solutions:
- For client components: Make sure you're using the
useSession
hook and checking the loading state - For server components: Ensure you're using
getServerSession
with the correct authOptions - Verify that the Providers wrapper is correctly set up in your layout
"MongoDB Connection Errors"
Problem: You see errors about MongoDB connection failures.
Solutions:
- Check your MONGODB_URI for typos
- Make sure you've whitelisted your IP address in MongoDB Atlas
- Verify that your database user has the correct permissions
- For local development, ensure MongoDB is running if you're using a local instance
Debugging Tip: Add console.log statements in your API routes to see what's happening. You can view these logs in your terminal where Next.js is running.
Step 20: Security Best Practices Explained
As you build authentication systems, security becomes crucial. Here are some best practices explained in simple terms:
1. Password Security
- Always hash passwords
- never store them as plain text
- Use a strong hashing algorithm like bcrypt (which we've implemented)
- Set a reasonable "salt rounds" value (10-12 is good for most applications)
2. Environment Variables
- Keep all secrets in
.env.local
files - Never commit these files to Git repositories
- Use different secrets in development and production
3. HTTPS
- Always use HTTPS in production
- NextAuth won't work properly without it in production environments
- Services like Vercel provide HTTPS automatically
4. Rate Limiting
- Limit how many login attempts are allowed
- This prevents brute force attacks where attackers try many passwords
A simple rate limiting implementation looks like this:
JavaScript1// This would go in your NextAuth API route
2import rateLimit from 'express-rate-limit';
3
4// Create a limiter: max 5 requests per minute
5const limiter = rateLimit({
6 windowMs: 60 * 1000, // 1 minute
7 max: 5, // 5 requests per window
8 standardHeaders: true,
9 legacyHeaders: false,
10});
11
12// Apply to your API routes
5. Security Headers
Next.js allows you to add security headers to your app. Create or update next.config.js
:
JavaScript1const nextConfig = {
2 headers: async () => {
3 return [
4 {
5 source: '/(.*)',
6 headers: [
7 {
8 key: 'X-Content-Type-Options',
9 value: 'nosniff',
10 },
11 {
12 key: 'X-Frame-Options',
13 value: 'DENY',
14 },
15 {
16 key: 'X-XSS-Protection',
17 value: '1; mode=block',
18 },
19 ],
20 },
21 ];
22 },
23};
24
25module.exports = nextConfig;
Security Tip: Regularly update your dependencies to get security patches. The
npm audit
command can help identify vulnerabilities.
Step 21: Deploying Your Secure Application
Understanding Deployment Requirements
Before deploying your authentication system to the internet, there are special considerations:
- Environment Variables: Production needs its own set of secure variables
- MongoDB Access: Your database needs to be accessible from your hosting provider
- OAuth Callback URLs: Must be updated to use your production domain
- HTTPS: Required for secure authentication
Getting Ready for Deployment
- Create a free account on Vercel (it's built by the same team that makes Next.js!)
- Push your code to a GitHub repository
- Make sure your MongoDB Atlas cluster allows connections from anywhere (or specifically from Vercel's IP range)
- Prepare your environment variables:
NEXTAUTH_URL
: Set to your production URL (e.g., https://my-auth-app.vercel.app)NEXTAUTH_SECRET
: Generate a new secure random string for production- OAuth credentials: You may need separate ones for production
Simple Deployment Steps
- Connect your GitHub repository to Vercel
- Add all your environment variables in the Vercel dashboard
- Click "Deploy"
- Once deployed, update your OAuth callback URLs in Google and GitHub to include your new domain:
https://your-domain.vercel.app/api/auth/callback/google
https://your-domain.vercel.app/api/auth/callback/github
- Test the authentication flow on your live site
Pro Tip: Use Vercel's "Preview Deployments" feature to test changes before they go live on your main domain.
Step 22: Next Steps and Advanced Features
Once you have your basic authentication system working, you might want to explore more advanced features:
Email Verification
Send verification emails to confirm user addresses:
- Add a
emailVerified
field to your User model - Install a package for sending emails:
npm install nodemailer
- Create an API route to send verification emails
- Add a callback to check verification status:
JavaScript1// In your NextAuth configuration
2callbacks: {
3 async signIn({ user, account }) {
4 // Allow OAuth sign-ins
5 if (account?.provider !== "credentials") {
6 return true;
7 }
8
9 // Check if email is verified for credentials
10 await dbConnect();
11 const userDoc = await User.findOne(
12 { email: user.email },
13 { emailVerified: 1 }
14 );
15
16 return userDoc?.emailVerified ? true : "/auth/verify-email";
17 },
18},
Password Reset
Implement a "forgot password" flow:
- Create a form where users can request password resets
- Generate a unique, time-limited token
- Send an email with a reset link
- Create a page where users can enter a new password using this token
Two-Factor Authentication (2FA)
Add an extra layer of security:
- Install 2FA libraries:
npm install otplib qrcode
- Generate and store a secret for each user
- Show a QR code for users to scan with authenticator apps
- Add a step in the login process to verify the 2FA code
Role-Based Access Control
Restrict different parts of your app to different user types:
- Add a
role
field to your User model (e.g., "user", "admin") - Add the role to the session in the JWT callback
- Check the role before allowing access to protected routes
JavaScript1// Add to your NextAuth configuration
2callbacks: {
3 async jwt({ token, user }) {
4 if (user) {
5 token.role = user.role;
6 }
7 return token;
8 },
9 async session({ session, token }) {
10 if (token && session.user) {
11 session.user.role = token.role;
12 }
13 return session;
14 },
15},
Then check the role in your components or middleware:
JavaScript1if (session?.user?.role !== "admin") {
2 return <p>Access denied. Admins only.</p>;
3}
Conclusion
Congratulations! You've learned how to implement a secure, production-ready authentication system in Next.js using NextAuth.js and MongoDB. Let's recap what you've accomplished:
- Created a Next.js application with authentication built in
- Set up OAuth providers (Google and GitHub) for social login
- Implemented email/password authentication with secure password hashing
- Connected to MongoDB for data storage
- Protected routes against unauthorized access
- Built user interface components for login, registration, and profile display
This foundation gives you a secure starting point for any web application that requires user accounts and authentication.
What to Learn Next
To continue building your skills, consider exploring:
- Advanced NextAuth.js features like callbacks and events
- More complex database schemas and relationships
- User profile customization and avatar uploads
- Role-based permissions and access control
- Enhanced security with Two-Factor Authentication
Helpful Resources
- NextAuth.js Documentation
- Next.js Documentation
- MongoDB Atlas Documentation
- OWASP Authentication Best Practices
Happy coding, and enjoy building secure applications with Next.js!
Beginner's Guide to Next.js Authentication with NextAuth.js and MongoDB (2025 Edition)
Introduction: What is Authentication and Why Do You Need It?
Ever visited a website that remembers who you are and shows your personal information? That's authentication in action! Authentication is like the digital version of checking someone's ID - it verifies that users are who they claim to be.
Adding authentication to your Next.js website is important because it:
- Keeps user information safe from unauthorized access
- Allows you to create personalized experiences for each user
- Protects private sections of your website
- Builds trust with your visitors who know their data is secure
- Helps comply with data protection regulations
NextAuth.js makes adding authentication to your Next.js app super easy, even if you're a beginner. It works with popular login methods like Google and GitHub, and you can also use traditional email/password login. Best of all, we'll connect it to MongoDB, a beginner-friendly database that's perfect for web applications!
What You'll Build in This Tutorial
By the end of this beginner-friendly guide, you'll create a Next.js application with:
- A login page with social login buttons (Google and GitHub)
- Secure email/password login that properly protects passwords
- Protected pages that only logged-in users can access
- User profiles showing information from their social accounts
- Data storage in MongoDB, a popular and free database
- Session management that keeps users logged in
![Authentication Flow Example]
What You'll Need Before Starting
- Basic knowledge of JavaScript and React (you don't need to be an expert!)
- Node.js installed on your computer (download from nodejs.org)
- A code editor like Visual Studio Code (it's free!)
- A free MongoDB Atlas account (we'll show you how to set this up)
- A free GitHub or Google account for setting up social login
Don't worry if you're new to some of these technologies - we'll explain each step clearly!
Step 1: Creating Your Next.js Project
Let's start with a fresh Next.js application:
Bash1npx create-next-app@latest my-secure-app
2cd my-secure-app
This command creates a new Next.js project with the latest features, including the App Router for enhanced routing capabilities.
Step 2: Installing NextAuth.js
Add NextAuth.js to your project dependencies:
Bashnpm install next-auth@latest @types/next-auth
The latest version includes improved TypeScript support and enhanced security features for your authentication system.
Step 3: Setting Up Environment Variables
When building apps with login features, you need to keep certain information secret, like API keys and passwords. Let's set those up:
- Create a file named
.env.local
in your project's main folder - Add the following lines to it:
Plain Text1NEXTAUTH_URL=http://localhost:3000 2NEXTAUTH_SECRET=your_secure_key_here 3MONGODB_URI=your_mongodb_connection_string 4GOOGLE_CLIENT_ID=your_google_id 5GOOGLE_CLIENT_SECRET=your_google_secret 6GITHUB_CLIENT_ID=your_github_id 7GITHUB_CLIENT_SECRET=your_github_secret
For the NEXTAUTH_SECRET
, you need a random, secure string. You can generate one by:
On Mac/Linux: Open Terminal and type:
Bashopenssl rand -base64 32
On Windows: Open PowerShell and type:
Powershell[Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
Copy the result and paste it as your NEXTAUTH_SECRET
.
Important for beginners: Never share your
.env.local
file or upload it to GitHub! This file contains secret information that should stay private. Next.js automatically prevents this file from being sent to the browser, keeping your secrets safe.
Step 4: Configuring OAuth Providers
Setting Up Google Authentication (with Screenshots)
Let's set up Google login for your app:
- Go to the Google Cloud Console
- Create a new project by clicking the project dropdown at the top of the page and selecting "New Project"
- Give your project a name like "My Auth App" and click "Create"
- Once your project is created, select it and go to the sidebar menu
- Click on "APIs & Services" > "Credentials"
- On the Credentials page, click the "Create Credentials" button and select "OAuth client ID"
- You might need to configure the consent screen first - click "Configure Consent Screen"
- Choose "External" (available to any Google user)
- Fill in the required app information (name, user support email, developer contact)
- You can leave most fields as default for testing
- Add your email as a test user
- Click "Save and Continue" through each section
- Back on the credentials page, click "Create Credentials" > "OAuth Client ID" again
- Select "Web Application" as the application type
- Give it a name like "Next.js Web Client"
- Under "Authorized JavaScript origins" add
http://localhost:3000
- Under "Authorized redirect URIs" add
http://localhost:3000/api/auth/callback/google
- Click "Create"
- A popup will show your Client ID and Client Secret - copy these
- Paste them into your
.env.local
file asGOOGLE_CLIENT_ID
andGOOGLE_CLIENT_SECRET
That's it! Your Google authentication is now set up for local development.
Setting Up GitHub Authentication (Easy Step-by-Step)
Now let's add GitHub login to your app:
- Sign in to your GitHub account
- Go to GitHub Developer Settings (Click your profile picture > Settings > Developer settings > OAuth Apps)
- Click the "New OAuth App" button
- Fill in the form:
- Application name: "My Next.js Auth App" (or any name you like)
- Homepage URL:
http://localhost:3000
- Application description: "A Next.js app with authentication" (optional)
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Click "Register application"
- On the next screen, you'll see your Client ID
- Click "Generate a new client secret"
- Copy both the Client ID and the new Client Secret
- Add them to your
.env.local
file asGITHUB_CLIENT_ID
andGITHUB_CLIENT_SECRET
Now your app can use both Google and GitHub for login!
Step 5: Creating the NextAuth API Route
Create app/api/auth/[...nextauth]/route.ts
:
TypeScript1import NextAuth from "next-auth";
2import GoogleProvider from "next-auth/providers/google";
3import GitHubProvider from "next-auth/providers/github";
4import CredentialsProvider from "next-auth/providers/credentials";
5import { compare } from "bcryptjs";
6import { MongoDBAdapter } from "@auth/mongodb-adapter";
7import clientPromise from "@/lib/mongodb-client";
8import User from "@/models/User";
9import dbConnect from "@/lib/mongodb";
10
11// Create a simplified MongoDB client for the adapter
12export const authOptions = {
13 adapter: MongoDBAdapter(clientPromise),
14 providers: [
15 GoogleProvider({
16 clientId: process.env.GOOGLE_CLIENT_ID!,
17 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
18 }),
19 GitHubProvider({
20 clientId: process.env.GITHUB_CLIENT_ID!,
21 clientSecret: process.env.GITHUB_CLIENT_SECRET!,
22 }),
23 CredentialsProvider({
24 name: "Email",
25 credentials: {
26 email: { label: "Email", type: "email" },
27 password: { label: "Password", type: "password" }
28 },
29 async authorize(credentials) {
30 if (!credentials?.email || !credentials?.password) {
31 return null;
32 }
33
34 // Connect to database
35 await dbConnect();
36
37 // Find user by email
38 const user = await User.findOne({ email: credentials.email });
39
40 if (!user || !user.password) {
41 return null;
42 }
43
44 // Compare provided password with stored hash
45 const isPasswordValid = await compare(
46 credentials.password,
47 user.password
48 );
49
50 if (!isPasswordValid) {
51 return null;
52 }
53
54 // Return user object if credentials are valid
55 return {
56 id: user._id.toString(),
57 email: user.email,
58 name: user.name,
59 image: user.image,
60 };
61 }
62 }),
63 ],
64 session: {
65 strategy: "jwt",
66 maxAge: 8 * 60 * 60, // 8 hours
67 },
68 callbacks: {
69 async session({ session, token }) {
70 if (token && session.user) {
71 session.user.id = token.sub;
72 // Add additional user data to the session if needed
73 // For example, if you want to add a role field:
74 // session.user.role = token.role;
75 }
76 return session;
77 },
78 },
79 pages: {
80 signIn: "/auth/signin",
81 error: "/auth/error",
82 },
83};
84
85const handler = NextAuth(authOptions);
86export { handler as GET, handler as POST };
This configuration includes:
- Multiple authentication providers
- Session management with JWT
- Custom callback functions for session handling
- Custom pages for the authentication flow
Step 6: Setting Up MongoDB with Mongoose
For persistent user data and credentials management, let's connect NextAuth.js to MongoDB using Mongoose:
Install MongoDB and Mongoose packages:
Bashnpm install mongodb mongoose @auth/mongodb-adapter
Set up your MongoDB connection string in
.env.local
:Plain TextMONGODB_URI=mongodb+srv://yourusername:yourpassword@yourcluster.mongodb.net/nextauth?retryWrites=true&w=majority
Note for beginners: Replace the connection string with your actual MongoDB connection string. You can get this from MongoDB Atlas (free tier available) or your local MongoDB installation.
- Create a database connection file in
lib/mongodb.js
: ```javascript import mongoose from 'mongoose';
const MONGODBURI = process.env.MONGODBURI;
if (!MONGODBURI) { throw new Error('Please define the MONGODBURI environment variable'); }
/**
- Global is used here to maintain a cached connection across hot reloads
- in development. This prevents connections growing exponentially
- during API Route usage. */ let cached = global.mongoose;
if (!cached) { cached = global.mongoose = { conn: null, promise: null }; }
async function dbConnect() { if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
Plain Text1const opts = { 2 bufferCommands: false, 3}; 4 5cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => { 6 return mongoose; 7});
}
cached.conn = await cached.promise; return cached.conn; }
export default dbConnect;
Plain Text4. Create user models in `models/User.js`:
javascript import mongoose from 'mongoose';
// Check if the User model already exists to prevent overwriting const UserSchema = new mongoose.Schema({ name: String, email: {
Plain Text1type: String, 2unique: true, 3sparse: true,
}, emailVerified: Date, image: String, password: String, });
export default mongoose.models.User || mongoose.model('User', UserSchema);
Plain Text5. Create your Account model in `models/Account.js`:
javascript import mongoose from 'mongoose';
const AccountSchema = new mongoose.Schema({ userId: {
Plain Text1type: mongoose.Schema.Types.ObjectId, 2ref: 'User',
}, type: String, provider: String, providerAccountId: String, refreshtoken: String, accesstoken: String, expiresat: Number, tokentype: String, scope: String, idtoken: String, sessionstate: String, });
// Compound index to ensure unique provider + providerAccountId AccountSchema.index({ provider: 1, providerAccountId: 1 }, { unique: true });
export default mongoose.models.Account || mongoose.model('Account', AccountSchema);
Plain Text6. Create your Session model in `models/Session.js`:
javascript import mongoose from 'mongoose';
const SessionSchema = new mongoose.Schema({ userId: {
Plain Text1type: mongoose.Schema.Types.ObjectId, 2ref: 'User',
}, expires: Date, sessionToken: {
Plain Text1type: String, 2unique: true,
}, });
export default mongoose.models.Session || mongoose.model('Session', SessionSchema);
Plain Text7. Create a VerificationToken model in `models/VerificationToken.js`:
javascript import mongoose from 'mongoose';
const VerificationTokenSchema = new mongoose.Schema({ identifier: String, token: {
Plain Text1type: String, 2unique: true,
}, expires: Date, });
// Compound index for identifier + token VerificationTokenSchema.index({ identifier: 1, token: 1 }, { unique: true });
export default mongoose.models.VerificationToken || mongoose.model('VerificationToken', VerificationTokenSchema);
Plain TextThese models form the foundation of your authentication system, storing users, their linked accounts, active sessions, and verification tokens for email verification.
Step 7: Building the SignIn Page
Create app/auth/signin/page.tsx
:
React TSX1"use client";
2
3import { useState } from "react";
4import { signIn, getProviders } from "next-auth/react";
5import { useRouter } from "next/navigation";
6import Image from "next/image";
7
8export default function SignIn() {
9 const [email, setEmail] = useState("");
10 const [password, setPassword] = useState("");
11 const [error, setError] = useState("");
12 const [loading, setLoading] = useState(false);
13 const [providers, setProviders] = useState<any>(null);
14 const router = useRouter();
15
16 useState(() => {
17 const loadProviders = async () => {
18 const providers = await getProviders();
19 setProviders(providers);
20 };
21 loadProviders();
22 }, []);
23
24 const handleEmailSignIn = async (e: React.FormEvent) => {
25 e.preventDefault();
26 setLoading(true);
27 setError("");
28
29 try {
30 const result = await signIn("credentials", {
31 redirect: false,
32 email,
33 password,
34 });
35
36 if (result?.error) {
37 setError("Invalid email or password");
38 } else {
39 router.push("/dashboard");
40 }
41 } catch (error) {
42 setError("An unexpected error occurred");
43 } finally {
44 setLoading(false);
45 }
46 };
47
48 return (
49 <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4">
50 <div className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg">
51 <div className="text-center">
52 <h1 className="text-3xl font-bold text-gray-900">Sign In</h1>
53 <p className="mt-2 text-gray-600">Access your secure dashboard</p>
54 </div>
55
56 {error && (
57 <div className="bg-red-50 text-red-500 p-3 rounded-lg text-sm">
58 {error}
59 </div>
60 )}
61
62 <form className="mt-8 space-y-6" onSubmit={handleEmailSignIn}>
63 <div>
64 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
65 Email address
66 </label>
67 <input
68 id="email"
69 name="email"
70 type="email"
71 autoComplete="email"
72 required
73 value={email}
74 onChange={(e) => setEmail(e.target.value)}
75 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
76 />
77 </div>
78
79 <div>
80 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
81 Password
82 </label>
83 <input
84 id="password"
85 name="password"
86 type="password"
87 autoComplete="current-password"
88 required
89 value={password}
90 onChange={(e) => setPassword(e.target.value)}
91 className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
92 />
93 </div>
94
95 <div>
96 <button
97 type="submit"
98 disabled={loading}
99 className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
100 >
101 {loading ? "Signing in..." : "Sign in with Email"}
102 </button>
103 </div>
104 </form>
105
106 <div className="mt-6">
107 <div className="relative">
108 <div className="absolute inset-0 flex items-center">
109 <div className="w-full border-t border-gray-300" />
110 </div>
111 <div className="relative flex justify-center text-sm">
112 <span className="px-2 bg-white text-gray-500">Or continue with</span>
113 </div>
114 </div>
115
116 <div className="mt-6 grid grid-cols-2 gap-3">
117 {providers &&
118 Object.values(providers)
119 .filter((provider: any) => provider.id !== "credentials")
120 .map((provider: any) => (
121 <button
122 key={provider.id}
123 onClick={() => signIn(provider.id, { callbackUrl: "/dashboard" })}
124 className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
125 >
126 {provider.id === "google" && (
127 <>
128 <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
129 <path
130 fill="currentColor"
131 d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
132 />
133 </svg>
134 Google
135 </>
136 )}
137 {provider.id === "github" && (
138 <>
139 <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
140 <path
141 fill="currentColor"
142 d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
143 />
144 </svg>
145 GitHub
146 </>
147 )}
148 </button>
149 ))}
150 </div>
151 </div>
152 </div>
153 </div>
154 );
155}
This enhanced sign-in page includes:
- Email/password authentication
- Social login options
- Form validation
- Error handling
- Loading states
- Responsive design with Tailwind CSS
Step 8: Creating a Provider Wrapper
To ensure NextAuth client components work properly, create a provider wrapper in app/providers.tsx
:
React TSX1"use client";
2
3import { SessionProvider } from "next-auth/react";
4
5export function Providers({ children }: { children: React.ReactNode }) {
6 return <SessionProvider>{children}</SessionProvider>;
7}
Then update your app/layout.tsx
:
React TSX1import { Providers } from "./providers";
2import "./globals.css";
3
4export const metadata = {
5 title: "Secure Next.js App",
6 description: "A secure Next.js application with NextAuth.js",
7};
8
9export default function RootLayout({
10 children,
11}: {
12 children: React.ReactNode;
13}) {
14 return (
15 <html lang="en">
16 <body>
17 <Providers>{children}</Providers>
18 </body>
19 </html>
20 );
21}
Step 9: Protecting Routes
Server-Side Protection
Create app/dashboard/page.tsx
:
React TSX1import { getServerSession } from "next-auth";
2import { authOptions } from "@/app/api/auth/[...nextauth]/route";
3import { redirect } from "next/navigation";
4import UserProfile from "@/components/UserProfile";
5
6export default async function Dashboard() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 redirect("/auth/signin");
11 }
12
13 return (
14 <div className="max-w-4xl mx-auto p-6">
15 <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
16 <div className="bg-white shadow rounded-lg p-6">
17 <UserProfile />
18
19 <div className="mt-8">
20 <h2 className="text-xl font-semibold mb-4">Your Secure Content</h2>
21 <p>This content is only visible to authenticated users.</p>
22 </div>
23 </div>
24 </div>
25 );
26}
Client-Side Protection with useSession Hook
Create components/UserProfile.tsx
:
React TSX1"use client";
2
3import { useSession, signOut } from "next-auth/react";
4import Image from "next/image";
5
6export default function UserProfile() {
7 const { data: session, status } = useSession();
8
9 if (status === "loading") {
10 return <div className="animate-pulse">Loading user profile...</div>;
11 }
12
13 if (!session) {
14 return <p>Please sign in to view your profile</p>;
15 }
16
17 return (
18 <div className="flex items-center space-x-4">
19 {session.user?.image ? (
20 <div className="relative w-16 h-16 rounded-full overflow-hidden">
21 <Image
22 src={session.user.image}
23 alt="Profile picture"
24 fill
25 className="object-cover"
26 />
27 </div>
28 ) : (
29 <div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center">
30 <span className="text-2xl text-gray-600">
31 {session.user?.name?.charAt(0) || session.user?.email?.charAt(0) || '?'}
32 </span>
33 </div>
34 )}
35
36 <div>
37 {session.user?.name && (
38 <h2 className="text-xl font-semibold">{session.user.name}</h2>
39 )}
40 {session.user?.email && (
41 <p className="text-gray-600">{session.user.email}</p>
42 )}
43 <button
44 onClick={() => signOut({ callbackUrl: "/" })}
45 className="mt-2 text-sm text-red-600 hover:text-red-800"
46 >
47 Sign out
48 </button>
49 </div>
50 </div>
51 );
52}
Step 10: Creating a Middleware for Multiple Protected Routes
For protecting multiple routes at once, create middleware.ts
in your project root:
TypeScript1import { NextResponse } from "next/server";
2import { NextRequest } from "next/server";
3import { getToken } from "next-auth/jwt";
4
5export async function middleware(request: NextRequest) {
6 const { pathname } = request.nextUrl;
7
8 // Protect these paths
9 const protectedPaths = ["/dashboard", "/profile", "/settings"];
10 const isPathProtected = protectedPaths.some((path) =>
11 pathname.startsWith(path)
12 );
13
14 if (isPathProtected) {
15 const token = await getToken({
16 req: request,
17 secret: process.env.NEXTAUTH_SECRET,
18 });
19
20 // Redirect to login if user is not authenticated
21 if (!token) {
22 const url = new URL(`/auth/signin`, request.url);
23 url.searchParams.set("callbackUrl", pathname);
24 return NextResponse.redirect(url);
25 }
26 }
27
28 return NextResponse.next();
29}
30
31export const config = {
32 matcher: ["/dashboard/:path*", "/profile/:path*", "/settings/:path*"],
33};
Advanced Security Practices
Implementing Password Hashing
Install bcryptjs:
Bashnpm install bcryptjs @types/bcryptjs
Create a utility function in utils/auth.ts
:
TypeScript1import { hash, compare } from "bcryptjs";
2
3export async function hashPassword(password: string): Promise<string> {
4 return await hash(password, 12);
5}
6
7export async function verifyPassword(
8 plainPassword: string,
9 hashedPassword: string
10): Promise<boolean> {
11 return await compare(plainPassword, hashedPassword);
12}
Implementing Rate Limiting
Install rate limiting packages:
Bashnpm install @upstash/redis @upstash/ratelimit
Create a rate limiter in your auth API:
TypeScript1import { Ratelimit } from "@upstash/ratelimit";
2import { Redis } from "@upstash/redis";
3
4// Create a new ratelimiter that allows 5 requests per minute
5const ratelimit = new Ratelimit({
6 redis: Redis.fromEnv(),
7 limiter: Ratelimit.slidingWindow(5, "1 m"),
8 analytics: true,
9});
10
11// In your auth route handler
12export async function POST(request: Request) {
13 const ip = request.headers.get("x-forwarded-for") || "unknown";
14 const { success } = await ratelimit.limit(ip);
15
16 if (!success) {
17 return new Response("Too many requests", { status: 429 });
18 }
19
20 // Continue with normal auth handling
21}
Testing Your Authentication System
Testing with Jest and React Testing Library
Install testing dependencies:
Bashnpm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
Create a test for your SignIn component:
TypeScript1// __tests__/SignIn.test.tsx
2import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3import SignIn from '@/app/auth/signin/page';
4import { signIn } from 'next-auth/react';
5
6// Mock NextAuth
7jest.mock('next-auth/react', () => ({
8 signIn: jest.fn(),
9 getProviders: jest.fn().mockResolvedValue({
10 google: { id: 'google', name: 'Google' },
11 }),
12}));
13
14// Mock Next Router
15jest.mock('next/navigation', () => ({
16 useRouter: jest.fn().mockReturnValue({
17 push: jest.fn(),
18 }),
19}));
20
21describe('SignIn Page', () => {
22 it('renders sign in form', async () => {
23 render(<SignIn />);
24
25 expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
26 expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
27 expect(screen.getByRole('button', { name: /sign in with email/i })).toBeInTheDocument();
28 });
29
30 it('handles form submission', async () => {
31 (signIn as jest.Mock).mockResolvedValueOnce({ error: null });
32
33 render(<SignIn />);
34
35 fireEvent.change(screen.getByLabelText(/email address/i), {
36 target: { value: 'test@example.com' },
37 });
38
39 fireEvent.change(screen.getByLabelText(/password/i), {
40 target: { value: 'password123' },
41 });
42
43 fireEvent.click(screen.getByRole('button', { name: /sign in with email/i }));
44
45 await waitFor(() => {
46 expect(signIn).toHaveBeenCalledWith('credentials', {
47 redirect: false,
48 email: 'test@example.com',
49 password: 'password123',
50 });
51 });
52 });
53});
Deploying Your Secure Application
Deployment Checklist
Before deploying to production:
- Update your environment variables on the production server
- Set
NEXTAUTH_URL
to your production URL - Generate a new strong
NEXTAUTH_SECRET
for production - Configure proper CORS settings
- Set up a database for persistance
- Ensure all callback URLs in your OAuth providers are updated to your production domain
- Implement HTTPS
Vercel Deployment
To deploy to Vercel:
- Push your code to GitHub
- Connect your repository to Vercel
- Configure environment variables in the Vercel dashboard
- Deploy your application
Common Problems and How to Fix Them
Even when following a guide step-by-step, you might run into some issues. Here are simple solutions to the most common problems:
"My Login Isn't Working"
If you click login buttons and nothing happens or you get errors:
- Check Your Environment Variables: Make sure your
.env.local
file has all the correct values and there are no typos - Verify Redirect URLs: Double-check that the callback URLs in Google and GitHub settings exactly match your app's URLs
- Look at Browser Console: Press F12 in your browser and check the Console tab for error messages
- Check NextAuth Secret: Make sure your NEXTAUTH_SECRET is set and doesn't contain any special characters
"I Keep Getting Logged Out"
If your login doesn't stay active between page refreshes:
- Session Provider: Make sure you've added the SessionProvider to your `layout.
Next Steps and Additional Features
Implementing Email Verification
Add email verification using a verification token:
TypeScript1// In your NextAuth configuration
2callbacks: {
3 async signIn({ user, account }) {
4 // Allow OAuth sign-ins
5 if (account?.provider !== "credentials") {
6 return true;
7 }
8
9 // Connect to the database
10 await dbConnect();
11
12 // Check if email is verified for credentials
13 const userDoc = await User.findOne(
14 { email: user.email },
15 { emailVerified: 1 }
16 );
17
18 return userDoc?.emailVerified ? true : "/auth/verify-email";
19 },
20},
Adding Two-Factor Authentication
For enhanced security, implement 2FA using:
Bashnpm install otplib qrcode
Conclusion
Congratulations! You've successfully implemented a robust authentication system in your Next.js application using NextAuth.js. This foundation gives you:
- Secure multi-provider authentication
- Protected routes on both client and server
- A customizable user experience
- Industry-standard security practices
Remember that security is an ongoing process. Keep your dependencies updated, follow security best practices, and consider implementing additional security measures as your application grows.