How to Implement JWT Authentication in Next.js 14

Fazal wahab
12 min readMar 15, 2024

--

Next.js 14: JWT Authentication and MongoDB Integration for Secure Web Apps

here’s a step-by-step guide on how to implement JWT authentication in Next.js 14:

1. **Database Connection**
First, establish a connection to your MongoDB database. This is done in the `dbconfige.tsx` file using the `mongoose.connect()` function.

```tsx
// File: src/app/dbconfig/dbconfige.tsx

import mongoose from ‘mongoose’;

export async function connect() {
try {
mongoose.connect(process.env.MONGO_URI!);
const connection = mongoose.connection;

connection.on(‘connected’, () => {
console.log(‘MongoDB connected successfully’);
})

connection.on(‘error’, (err) => {
console.log(‘MongoDB connection error. Please make sure MongoDB is running. ‘ + err);
process.exit();
})

} catch (error) {
console.log(‘Something goes wrong!’);
console.log(error);

}
}
```

2. **User Registration**
In the `signup` route, a new user is created. The user’s password is hashed using `bcryptjs` before being stored in the database. An email verification token is also generated and sent to the user’s email.

```tsx
// File: src/app/api/user/signup/route.ts

import User from “@/app/modols/usermodule”;
import { NextRequest, NextResponse } from “next/server”;
import bcryptjs from “bcryptjs”;
import { connect } from “@/app/dbcomfig/dbconfige”;
import { sendEmail } from “@/helpers/mailer”;

connect();

export async function POST(request: NextRequest) {
try {
const reqBody = await request.json();
const { username, email, password } = reqBody;

const existingUser = await User.findOne({ email });
if (existingUser) {
return NextResponse.json({ error: “User already exists” }, { status: 400 });
}

const salt = await bcryptjs.genSalt(10);
const hashedPassword = await bcryptjs.hash(password, salt);

const newUser = new User({
username,
email,
password: hashedPassword
});

const savedUser = await newUser.save();

const verificationToken = await bcryptjs.hash(savedUser._id.toString(), 10);
const templateName = “verification_template.html”;
const subject = “Email Verification”;
await sendEmail(email, subject, { verificationLink: `${process.env.DOMAIN}/verifyemail?token=${verificationToken}` }, templateName);

return NextResponse.json({
message: “User created successfully. Verification email sent.”,
success: true,
savedUser
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

```

3. **Email Verification**
When the user clicks the verification link in their email, the `verifyemail` route is hit. The token is verified and the user’s email is marked as verified in the database.

```tsx
// File: src/app/api/user/verifyemail/route.ts

import { connect } from “@/app/dbcomfig/dbconfige”;
import User from “@/app/modols/usermodule”;

import { NextRequest, NextResponse } from “next/server”;

connect();

export async function POST(request: NextRequest) {
try {
const reqBody = await request.json();
const { token, email } = reqBody;

const user = await User.findOne({ verifyToken: token, verifyTokenExpiry: { $gt: Date.now() } });

if (!user) {
return NextResponse.json({ error: “Invalid token” }, { status: 400 });
}

user.isVerfied = true;
user.verifyToken = undefined;
user.verifyTokenExpiry = undefined;
await user.save();

return NextResponse.json({
message: “Email verified successfully”,
success: true
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

```

4. **User Login**
In the `login` route, the user’s credentials are verified. If they are correct, a JWT token is generated and sent back to the user.

```tsx
// File: src/app/api/user/login/route.ts

import { NextRequest, NextResponse } from “next/server”;
import bcryptjs from “bcryptjs”;
import jwt from “jsonwebtoken”;
import { connect } from “@/app/dbcomfig/dbconfige”;
import User from “@/app/modols/usermodule”;

connect()

export async function POST(request: NextRequest){
try {

const reqBody = await request.json()
const {email, password} = reqBody;

const user = await User.findOne({email})
if(!user){
return NextResponse.json({error: “User does not exist”}, {status: 400})
}




const validPassword = await bcryptjs.compare(password, user.password)
if(!validPassword){
return NextResponse.json({error: “Invalid password”}, {status: 400})
}



const tokenData = {
id: user._id,
username: user.username,
email: user.email
}

const token = await jwt.sign(tokenData, process.env.TOKEN_SECRET!, {expiresIn: “1d”})

const response = NextResponse.json({
message: “Login successful”,
success: true,
})
response.cookies.set(“token”, token, {
httpOnly: true,

})
return response;

} catch (error: any) {
return NextResponse.json({error: error.message}, {status: 500})
}
}

```

5. **User Logout**
In the `logout` route, the user’s JWT token is cleared.

```tsx
// File: src/app/api/user/logout/route.ts

import { NextResponse} from “next/server”;

export async function GET(){
try {
const reqest= await NextResponse.json({
massage:’lagout succsefully’,
sucsee:true
})

reqest.cookies.set(“token”, ‘’ ,{ httpOnly: true , expires: new Date(0)});

return reqest

} catch (error) {
return NextResponse.json({massage:’your requast is not complete’})
}
}

```

6. **Password Reset**
In the `forgot-password` route, a password reset token is generated and sent to the user’s email. In the `reset-password` route, the token is verified and the user’s password is updated.

```tsx
// File: src/app/api/user/forgot-password/route.ts

import jwt from “jsonwebtoken”;

import User from ‘@/app/modols/usermodule’;
import { NextRequest, NextResponse } from ‘next/server’;
import { sendEmail } from “@/helpers/mailer”;

export async function POST(request: NextRequest){
try {
const reqBody = await request.json()
const {email} = reqBody;

const user = await User.findOne({email})
if(!user){
return NextResponse.json({error: “User does not exist”}, {status: 400})
}

const token = await jwt.sign({ userId: user._id }, process.env.TOKEN_SECRET!, {expiresIn: “1d”})
const templateName = “password_reset_email_template.html”;
const subject = “Password Reset”;

await sendEmail(email, subject, { domsn: process.env.DOMAIN, token }, templateName);

return NextResponse.json({ message: ‘Password reset email sent successfully’ });
} catch (error: any) {
return NextResponse.json({error: error.message}, {status: 500})
}
}

// File: src/app/api/user/reset-password/route.ts

import jwt from ‘jsonwebtoken’;
import bcryptjs from ‘bcryptjs’;
import User from ‘@/app/modols/usermodule’;
import { NextRequest, NextResponse } from ‘next/server’;

export async function POST(request: NextRequest) {
try {
const reqBody = await request.json();
const { token, password } = reqBody;

const decoded = jwt.verify(token, process.env.TOKEN_SECRET!) as { userId: string };

const user = await User.findById(decoded.userId);
if (!user) {
return NextResponse.json({ error: “User does not exist” }, { status: 400 });
}

const hashedPassword = await bcryptjs.hash(password, 10);
user.password = hashedPassword;
await user.save();

return NextResponse.json({ message: ‘Password reset successful’ });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

```

7. **Client-Side**
On the client-side, you can create a form for the user to enter their email and request a password reset. This can be done in the `page.tsx` file.

```tsx
7. **Forgot Password Page**
Create a new file `src/app/forgotpassword/page.tsx` and add the following code:

```tsx
import React, { useState } from ‘react’;
import axios from ‘axios’;

const Page = () => {
const [email, setEmail] = useState(‘’);
const [message, setMessage] = useState(‘’);
const [error, setError] = useState(‘’);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

try {
const response = await axios.post(‘/api/user/forgot-password’, { email });
setMessage(response.data.message);
setError(‘’);
} catch (error:any) {
setMessage(‘’);
setError(error.response.data.error);
}
};

return (
<div className=”flex flex-col items-center mt-8">
<h2 className=”text-2xl font-bold mb-4">Forgot Password</h2>
<form onSubmit={handleSubmit} className=”w-64">
<input
type=”email”
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder=”Enter your email”
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
<button type=”submit” className=”w-full px-4 py-2 mt-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600 focus:outline-none”>
Submit
</button>
</form>
{message && <p className=”mt-4 text-green-500">{message}</p>}
{error && <p className=”mt-4 text-red-500">{error}</p>}
</div>
);
};

export default Page;
```

8. **Email Verification**
Create a new file `src/app/api/user/verifyemail/route.ts` and add the following code:

```tsx
import { connect } from “@/app/dbcomfig/dbconfige”;
import User from “@/app/modols/usermodule”;

import { NextRequest, NextResponse } from “next/server”;

connect();

export async function POST(request: NextRequest) {
try {
const reqBody = await request.json();
const { token, email } = reqBody;

const user = await User.findOne({ verifyToken: token, verifyTokenExpiry: { $gt: Date.now() } });

if (!user) {
return NextResponse.json({ error: “Invalid token” }, { status: 400 });
}

user.isVerfied = true;
user.verifyToken = undefined;
user.verifyTokenExpiry = undefined;
await user.save();

return NextResponse.json({
message: “Email verified successfully”,
success: true
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

```

9. **Database Configuration**
Create a new file `src/app/dbcomfig/dbconfige.tsx` and add the following code:

```tsx
import mongoose from ‘mongoose’;

export async function connect() {
try {
mongoose.connect(process.env.MONGO_URI!);
const connection = mongoose.connection;

connection.on(‘connected’, () => {
console.log(‘MongoDB connected successfully’);
})

connection.on(‘error’, (err) => {
console.log(‘MongoDB connection error. Please make sure MongoDB is running. ‘ + err);
process.exit();
})

} catch (error) {
console.log(‘Something goes wrong!’);
console.log(error);

}
}

```

10. **Login Page**
Create a new file `src/app/login/page.tsx` and add the following code:

```tsx
import React, { useState, useEffect } from “react”;

import axios from “axios”;
import { toast } from “react-toastify”;
import { useRouter } from “next/navigation”;

export default function Page () {
const router = useRouter();
const [user, setUser] = useState({ email: “”, password: “” });
const [buttonDisabled, setButtonDisabled] = useState(true);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(“”);

const handleLogin = async () => {
setLoading(true);
try {
const response = await axios.post(“/api/user/login”, user);
toast.success(“Login successful”);
router.push(“/profile”);
} catch (error:any) {
if (error.response && error.response.status === 400) {
setErrorMessage(“Invalid email or password”);
} else {
toast.error(“An error occurred. Please try again later.”);
}
} finally {
setLoading(false);
}
};

useEffect(() => {
setButtonDisabled(!(user.email && user.password));
}, [user.email, user.password]);

return (
<div className=”flex flex-col items-center mt-8">
<h1 className=”text-2xl font-bold”>{loading ? ‘Processing’ : ‘Login’}</h1>
<hr className=”w-full my-4" />
<div className=”w-64">
<label htmlFor=”email” className=”block mb-1 font-medium text-gray-700">Email</label>
<input
id=”email”
type=”email”
placeholder=”Email”
value={user.email}
onChange={(e) => setUser({ …user, email: e.target.value })}
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
<label htmlFor=”password” className=”block mb-1 font-medium text-gray-700">Password</label>
<input
id=”password”
type=”password”
placeholder=”Password”
value={user.password}
onChange={(e) => setUser({ …user, password: e.target.value })}
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
{errorMessage && <p className=”text-red-500 mb-2">{errorMessage}</p>}
<button
onClick={handleLogin}
disabled={buttonDisabled}
className={`w-full px-4 py-2 mt-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600 focus:outline-none ${buttonDisabled ? ‘cursor-not-allowed opacity-50’ : ‘’}`}
>
{loading ? ‘Logging in…’ : ‘Login’}
</button>
</div>
<hr className=”w-full my-4" />
<a href=”/forgotpassword” className=”text-blue-500 hover:underline”>Forgot Password?</a>
<p className=”mt-2">Not registered? <a href=”/signup” className=”text-blue-500 hover:underline”>Sign up here</a></p>
</div>
);
}
```

11. **User Model**
Create a new file `src/app/modols/usermodule.tsx` and add the following code:

```tsx
import mongoose from “mongoose”;
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, ‘Please provide a username’],
unique: true
},
email: {
type: String,
required: [true, ‘Please provide an email’],
unique: true
},
password: {
type: String,
required: [true, ‘Please provide a password’],
},
isvirefd: {
type: Boolean,
default: false,
},
isAdmain: {
type: Boolean,
default: false,
},
forgotpasswardtoken: String,
forgotpasswardtokenexpiry: Date,
virifeytokien: String,
virifeytokienexpiry: Date
});

const User = mongoose.models.users || mongoose.model(“users”, userSchema);

export default User;
```

12. **Profile Page**
Create a new file `src/app/profile/page.tsx` and add the following code:

```tsx
import React from ‘react’;
import axios from ‘axios’;
import { useRouter } from ‘next/navigation’;
import { toast } from ‘react-toastify’;

function Page () {
const router = useRouter();

const handleLogout = async () => {

try {
await axios.get(‘/api/user/logout’);
toast.success(‘Logged out successfully’);
router.push(‘/login’);
} catch (error) {
console.error(‘Error during logout:’, error);
toast.error(‘An error occurred during logout’);
}
};

return (
<div className=”flex flex-col items-center mt-8">
<h1 className=”text-2xl font-bold mb-4">Profile</h1>
<hr className=”w-full my-2 border-gray-300" />
<h1 className=”text-xl font-semibold my-4">Profile Page</h1>
<hr className=”border-gray-300" />
<button
className=”px-4 py-2 bg-blue-500 text-white rounded-md mt-4 hover:bg-blue-600"
onClick={handleLogout}
>
Logout
</button>
</div>
);
}

export default Page;
```

13. **Profile ID Page**
Create a new file `src/app/profile/[id]/page.tsx` and add the following code:

```tsx
import React from ‘react’;

function Page ({ params }: any) {
return (
<div className=”flex flex-col items-center mt-8">
<h1 className=”text-2xl font-bold mb-4">Profile</h1>
<hr className=”w-full my-2" />
<h1 className=”text-xl font-semibold my-4">
UserProfile Page <span className=”text-blue-500">{params.id}</span>
</h1>
</div>
);
}

export default Page;
```

14. **Reset Password Page**
Create a new file `src/app/resetpassword/page.tsx` and add the following code:

```tsx
‘use client’
import axios from ‘axios’;
import Link from ‘next/link’;
import { useSearchParams } from “next/navigation”;
import { useEffect, useState } from ‘react’;

export default function Page() {

const [token, setToken] = useState(‘’);
const [error, setError] = useState(false);
const [password, setPassword] = useState(‘’);
const [loading, setLoading] = useState(true);
const [resetSuccess, setResetSuccess] = useState(false);
const searchparms = useSearchParams()
const resetPassword = async () => {
try {
await axios.post(‘/api/user/reset-password’, { token, password });
setResetSuccess(true);
} catch (error: any) {
setError(true);
console.log(error.response.data);
}
};

useEffect(() => {
const Token = searchparms.get(‘token’);
if (Token) {
setToken(Token);
}
setLoading(false);
}, [searchparms]);

if (loading) {
return <div>Loading…</div>;
}

return (
<div className=”flex flex-col items-center justify-center py-2">
<h1 className=”text-4xl font-bold mb-5">Reset Password</h1>

{resetSuccess && (
<div className=”text-center”>
<h2 className=”text-2xl mb-4">Password Reset Successful</h2>
<Link href=”/login” className=”text-blue-500 hover:underline”>
Login
</Link>
</div>
)}

{error && (
<div className=”text-center”>
<h2 className=”text-2xl bg-red-500 text-white px-4 py-2 rounded-md mb-4">Error</h2>
</div>
)}

{!resetSuccess && !error && (
<form onSubmit={(e) => { e.preventDefault(); resetPassword(); }} className=”flex flex-col gap-4">
<label htmlFor=”password” className=”text-lg font-semibold”>New Password:</label>
<input
type=”password”
id=”password”
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className=”px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
<button
type=”submit”
className=”px-6 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none”
>
Reset Password
</button>
</form>
)}
</div>

);
}

```

15. **Signup Page**
Create a new file `src/app/signup/page.tsx` and add the following code:

```tsx
‘use client’
import React, { useEffect, useState } from ‘react’;
import axios from ‘axios’;
import Link from ‘next/link’;
import { toast } from ‘react-toastify’;
import { useRouter } from ‘next/navigation’;

export default function Page () {
const router = useRouter();
const [user, setUser] = useState({
email: ‘’,
password: ‘’,
username: ‘’,
});
const [buttonDisabled, setButtonDisabled] = useState(false);
const [loading, setLoading] = useState(false);
const [isEmailRegistered, setIsEmailRegistered] = useState(false);
const [isEmailSent, setIsEmailSent] = useState(false);
const onSignup = async () => {
try {
setLoading(true);
const response = await axios.post(‘/api/user/signup’, user);
setIsEmailSent(true);
console.log(‘Signup success’, response.data);
toast.success(‘Signup successful! Please login to your account.’);
router.push(‘/login’);
} catch (error:any) {
console.log(‘Signup failed’, error.message);
if (
error.response &&
error.response.status === 400 &&
error.response.data.error === ‘User already exists’
) {
setIsEmailRegistered(true);
} else {
setIsEmailRegistered(false);
toast.error(error.message);
}
} finally {
setLoading(false);
}
};

useEffect(() => {
if (user.email.length > 0 && user.password.length > 0 && user.username.length > 0) {
setButtonDisabled(false);
} else {
setButtonDisabled(true);
}
}, [user]);

return (
<div className=”flex flex-col items-center mt-8">
<h1 className=”text-2xl font-bold”>{loading ? “processing” : “signup”}</h1>
<hr className=”w-full my-4" />
{isEmailRegistered && (
<p className=”text-red-500 mb-4">You have already registered. Please login to your account.</p>
)}
<div className=”w-64">
<label htmlFor=”username” className=”block mb-1 font-medium text-gray-700">
Username
</label>
<input
id=”username”
type=”text”
placeholder=”Username”
value={user.username}
onChange={(e) => setUser({ …user, username: e.target.value })}
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
<label htmlFor=”email” className=”block mb-1 font-medium text-gray-700">
Email
</label>
<input
id=”email”
type=”email”
placeholder=”Email”
value={user.email}
onChange={(e) => {
setUser({ …user, email: e.target.value });
setIsEmailRegistered(false);
}}
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
<label htmlFor=”password” className=”block mb-1 font-medium text-gray-700">
Password
</label>
<input
id=”password”
type=”password”
placeholder=”Password”
value={user.password}
onChange={(e) => setUser({ …user, password: e.target.value })}
className=”w-full px-4 py-2 mb-2 border rounded-lg focus:outline-none focus:border-blue-500"
/>
<button
onClick={onSignup}
disabled={buttonDisabled}
className={`w-full px-4 py-2 mt-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600 focus:outline-none ${
buttonDisabled ? ‘cursor-not-allowed opacity-50’ : ‘’
}`}
>
{loading ? ‘Signing up…’ : ‘Signup’}
</button>
</div>
<Link href=”/login” className=”text-blue-500 hover:underline mt-4">
Already registered? Log in here
</Link>
</div>
);
}

**Step 16: Create a new file named `verifyemail.tsx` in the `src/app` directory.**

```tsx
‘use client’

import Link from ‘next/link’;
import { useSearchParams } from “next/navigation”;
import { useEffect, useState } from ‘react’;

const Page = () => {
const [token, setToken] = useState(‘’);
const searchParams = useSearchParams();
const [loading, setLoading] = useState(true);

useEffect(() => {
const Token = searchParams.get(‘token’);
if (Token) {
setToken(Token);
}
setLoading(false);
}, [searchParams]);

if (loading) {
return <div>Loading…</div>;
}

return (
<div className=”flex flex-col items-center mt-8">
<h2 className=”text-2xl font-bold mb-4">Email Verification</h2>
<p>Hello,</p>
<p>Please click the following link to verify your email</p>
<Link href=”/login”className=”text-blue-500 underline hover:text-blue-700 focus:outline-none”>

Email Verification complete please login to your account

</Link>
<p>If you didn t request this verification you can ignore this email</p>
<p>Thank you!</p>
</div>
);
};

export default Page;
```

**Step 17: Create a new file named `mailer.ts` in the `src/helpers` directory.**

```tsx
import nodemailer from “nodemailer”;
import fs from “fs”;
import path from “path”;

const gmailUser = process.env.GMAIL_USER as string;
const gmailPassword = process.env.GMAIL_PASSWORD as string;

if (!gmailUser || !gmailPassword) {
throw new Error(
“Required environment variables GMAIL_USER or GMAIL_PASSWORD are not set.”
);
}

const transporter = nodemailer.createTransport({
service: “gmail”,
auth: {
user: gmailUser,
pass: gmailPassword,
},
});

function loadTemplate(templateName: string, data: any): string {
const templatePath = path.join(
process.cwd(),
“public/email-templates”,
`${templateName}`
);
let templateContent = fs.readFileSync(templatePath, “utf8”);

Object.keys(data).forEach((key) => {
templateContent = templateContent.replace(
new RegExp(`{${key}}`, “g”),
data[key]
);
});

return templateContent;
}

export async function sendEmail(
recipientEmail: string,
subject: string,
data: any,
templateName: string
) {
const htmlContent = loadTemplate(templateName, data);

if (!transporter) {
throw new Error(“Transporter is not defined.”);
}

try {
const info = await transporter.sendMail({
from: `”manningcompany” <${gmailUser}>`,
to: recipientEmail,
subject: subject,
html: htmlContent,
});

console.log(`Email sent to ${recipientEmail}: ${info.messageId}`);
} catch (error) {
console.error(“Error sending email”, error);
throw error;
}
}

```

**Step 18: Create a new file named `.env` in the root directory.**

```env
MONGO_URI=’mongodb+srv://techmade:CAHpt57gFDFZzVwb@techmade.lnfuxmh.mongodb.net/manning-project’
TOKEN_SECRET=
DOMAIN=
GMAIL_USER=
GMAIL_PASSWORD=
```

Please replace the `TOKEN_SECRET`, `DOMAIN`, `GMAIL_USER`, and `GMAIL_PASSWORD` with your actual values. These are environment variables that your application will use.

That’s it! You have successfully written the code for email verification and sending emails. Please make sure to install all the necessary packages for your application to run successfully. If you have any other questions, feel free to ask!

--

--

Fazal wahab

Next.js-13 Full-Stack | eCommerce Pro 🛒 | Code & Creativity