Basic JWT Authentication
Integrate Basic JWT authentication into your Next.js project with Easy Auth. Follow this guide for a seamless setup.
Introduction
In this guide, you'll learn how to implement basic JWT authentication in your application. We'll cover the necessary steps to set up JWT, handle user Authentication.
1. Install Necessary Packages
npm install jsonwebtoken bcryptjs jose cookie-parser mongoose next-connect
2. Setup .env
.env
MONGODB_URI=mongodb://localhost:27017/your_database_name
JWT_SECRET=your_jwt_secret_key
3. Connect with Database
src/lib/dbConnect.js
import mongoose from "mongoose";
export const dbConnect = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log("Connected to MongoDB");
} catch (error) {
console.log("Error connecting to MongoDB: ", error);
}
};
4. Create user model
src/models/User.js
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
export default mongoose.models.User || mongoose.model("User", UserSchema);
5. Create API Endpoints
1. Signup
src/app/api/signup/route.js
import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "@/models/User";
import { dbConnect } from "@/lib/dbConnect";
dbConnect();
export async function POST(req) {
try {
const { name, email, password } = await req.json();
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return NextResponse.json(
{ message: "User already exists" },
{ status: 400 },
);
}
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10);
// Create a new user
const user = await User.create({ name, email, password: hashedPassword });
// Generate a JWT token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET);
// Set the JWT token as a cookie
const response = NextResponse.json(
{ message: "User registered successfully" },
{ status: 201 },
);
response.cookies.set("token", token, {
httpOnly: true,
secure: true,
maxAge: 3600,
});
return response;
} catch (error) {
return NextResponse.json(
{ message: "Error registering user" },
{ status: 500 },
);
}
}
2. Login
src/app/api/login/route.js
import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "@/models/User";
import { dbConnect } from "@/lib/dbConnect";
dbConnect();
export async function POST(req) {
try {
const { email, password } = await req.json();
// Check if user exists
const user = await User.findOne({ email });
if (!user) {
return NextResponse.json(
{ message: "Invalid email or password" },
{ status: 400 },
);
}
// Check if password is correct
const isPasswordCorrect = await bcrypt.compare(password, user.password);
if (!isPasswordCorrect) {
return NextResponse.json(
{ message: "Invalid email or password" },
{ status: 400 },
);
}
// Generate a JWT token
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET);
// Set the JWT token as a cookie
const response = NextResponse.json(
{ message: "Logged in successfully" },
{ status: 200 },
);
response.cookies.set("token", token, {
httpOnly: true,
secure: true,
maxAge: 3600,
});
return response;
} catch (error) {
return NextResponse.json({ message: "Error logging in" }, { status: 500 });
}
}
3. Logout
src/app/api/logout/route.js
import { NextResponse } from "next/server";
export async function POST() {
const response = NextResponse.json(
{ message: "Logged out successfully" },
{ status: 200 },
);
response.cookies.set("token", "", {
httpOnly: true,
secure: true,
maxAge: 0,
});
return response;
}
4. Delete Account
src/app/api/delete-account/route.js
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import User from "@/models/User";
import { dbConnect } from "@/lib/dbConnect";
dbConnect();
export async function DELETE(req) {
try {
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json(
{ message: "Not authenticated" },
{ status: 401 },
);
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
await User.findByIdAndDelete(decoded.userId);
const response = NextResponse.json(
{ message: "Account deleted successfully" },
{ status: 200 },
);
response.cookies.set("token", "", {
httpOnly: true,
secure: true,
maxAge: 0,
});
return response;
} catch (error) {
return NextResponse.json(
{ message: "Error deleting account" },
{ status: 500 },
);
}
}
5. Profile
src/app/api/profile/route.js
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import User from "@/models/User";
import { dbConnect } from "@/lib/dbConnect";
dbConnect();
export async function GET(req) {
try {
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json(
{ message: "Not authenticated" },
{ status: 401 },
);
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select("-password");
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
return NextResponse.json(user, { status: 200 });
} catch (error) {
return NextResponse.json(
{ message: "Error fetching user data" },
{ status: 500 },
);
}
}
6. Create Forms
1. Signup Form
src/components/SignupForm/SignupForm.jsx
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { dbConnect } from "@/lib/dbConnect";
const SignupForm = () => {
const [loading, setLoading] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = async (event) => {
event.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email, password }),
});
const data = await res.json();
if (res.status === 201) {
// Redirect to profile page or show success message
console.log(data.message);
window.location.href = "/profile";
} else {
setError(data.message);
setLoading(false);
}
} catch (error) {
setError("An unexpected error occurred");
setLoading(false);
}
};
return (
<div className="flex items-center justify-center h-screen bg-black text-white">
<div className="rounded-xl border border-[#27272A] bg-card text-card-foreground shadow mx-auto max-w-sm">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold tracking-tight text-2xl">Sign Up</h3>
<p className="text-sm text-muted-foreground">
Enter your information to create an account
</p>
</div>
<div className="p-6 pt-0">
<form onSubmit={handleSubmit} method="POST">
<div className="grid gap-4">
<div className="grid gap-2">
<label
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="name"
>
Name
</label>
<input
type="text"
className="flex h-9 w-full rounded-md border border-[#27272A] border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Muhammad Kaif Nazeer"
required
/>
</div>
<div className="grid gap-2">
<label
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="email"
>
Email
</label>
<input
type="email"
className="flex h-9 w-full rounded-md border border-[#27272A] border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
id="email"
placeholder="m@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<label
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="password"
>
Password
</label>
<input
type="password"
className="flex h-9 w-full rounded-md border border-[#27272A] border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<LoaderButton loading={loading}>Register</LoaderButton>
</div>
{error && (
<div className="border rounded-lg border-[#27272A] px-3 py-2 w-max mt-4">
{error}
</div>
)}
</form>
<div className="mt-4 text-center text-sm">
Already have an account?{" "}
<Link className="underline" href="/login">
Login
</Link>
</div>
</div>
</div>
</div>
);
};
const LoaderButton = ({ loading, children, ...props }) => {
return (
<button
{...props}
className={`inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-[#0DDD50] text-primary-foreground shadow hover:bg-[#0DDD50]/90 h-9 px-4 py-2 w-full ${props.className} ${loading ? "bg-[#0DDD50]/70 text-white/80" : ""}`}
disabled={loading || props.disabled}
>
{loading && <div className="buttonLoader mr-2"></div>}
{children}
</button>
);
};
const styles = `
.buttonLoader {
display: inline-block;
border: 3px solid rgba(255, 255, 255, 0.6);
border-top-color: rgba(255, 255, 255, 1);
border-radius: 50%;
width: 18px;
height: 18px;
animation: blspin 1s linear infinite;
}
@keyframes blspin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`;
const injectStyles = () => {
if (typeof document !== "undefined") {
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
}
};
injectStyles();
export default SignupForm;
2. Login Form
src/components/LoginForm/LoginForm.jsx
"use client";
import React, { useState } from "react";
import Link from "next/link";
const LoginForm = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (event) => {
event.preventDefault();
setLoading(true);
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error("Failed to login");
}
const data = await response.json();
localStorage.setItem("token", data.token);
window.location.href = "/profile";
} catch (error) {
setError("Invalid email or password");
setLoading(false);
}
};
return (
<>
<div className="flex items-center justify-center h-screen bg-black text-white">
<div className="rounded-xl border border-[#27272A] bg-card text-card-foreground shadow mx-auto max-w-sm">
<div className="flex flex-col space-y-1.5 p-6">
<h3 className="font-semibold tracking-tight text-2xl">Login</h3>
<p className="text-sm text-muted-foreground">
Enter your email below to login to your account
</p>
</div>
<div className="p-6 pt-0">
<form onSubmit={handleSubmit}>
<div className="grid gap-4">
<div className="grid gap-2">
<label
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="email"
>
Email
</label>
<input
type="email"
className="flex h-9 w-full rounded-md border border-[#27272A] border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
id="email"
placeholder="m@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<label
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="password"
>
Password
</label>
<input
type="password"
className="flex h-9 w-full rounded-md border border-[#27272A] border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<LoaderButton loading={loading}>Login</LoaderButton>
</div>
{error && (
<div className="border rounded-lg border-[#27272A] px-3 py-2 w-max mt-4">
{error}
</div>
)}
</form>
<div className="mt-4 text-center text-sm">
Don't have an account?{" "}
<Link className="underline" href="/signup">
Sign up
</Link>
</div>
</div>
</div>
</div>
</>
);
};
const LoaderButton = ({ loading, children, ...props }) => {
return (
<button
{...props}
className={`inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-[#0DDD50] text-primary-foreground shadow hover:bg-[#0DDD50]/90 h-9 px-4 py-2 w-full ${props.className} ${loading ? "bg-[#0DDD50]/70 text-white/80" : ""}`}
disabled={loading || props.disabled}
>
{loading && <div className="buttonLoader mr-2"></div>}
{children}
</button>
);
};
const styles = `
.buttonLoader {
display: inline-block;
border: 3px solid rgba(255, 255, 255, 0.6);
border-top-color: rgba(255, 255, 255, 1);
border-radius: 50%;
width: 18px;
height: 18px;
animation: blspin 1s linear infinite;
}
@keyframes blspin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`;
const injectStyles = () => {
if (typeof document !== "undefined") {
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
}
};
injectStyles();
export default LoginForm;
7. Create Profile Page
src/app/profile/page.jsx
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
const ProfilePage = () => {
const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
const [user, setUser] = useState(null);
const [error, setError] = useState("");
const router = useRouter();
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await fetch("/api/profile", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch user data");
}
const data = await response.json();
setUser(data);
setLoading(false);
} catch (error) {
console.log("Error fetching user data: ", error);
// router.push("/login");
}
};
fetchUserData();
}, []);
if (loading) {
return <Loader />;
}
const handleLogout = async () => {
setLoggingOut(true);
try {
const res = await fetch("/api/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});
if (res.status === 200) {
router.push("/login"); // Redirect to login after logout
} else {
setError("Failed to log out");
}
} catch (error) {
setError("An unexpected error occurred");
}
};
const handleDeleteAccount = async () => {
setDeleting(true);
try {
const res = await fetch("/api/delete-account", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});
if (res.status === 200) {
router.push("/signup"); // Redirect to signup after account deletion
} else {
setError("Failed to delete account");
}
} catch (error) {
setError("An unexpected error occurred");
}
};
if (!user) {
return <div>Loading...</div>;
}
return (
<div className="flex min-h-screen items-center justify-center bg-black text-white px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto w-full max-w-md rounded-xl border border-[#27272A] p-6 shadow-lg">
<div className="space-y-4 text-center">
<div className="flex items-center justify-center">
<span className="relative flex shrink-0 overflow-hidden rounded-full h-16 w-16 items-center justify-center bg-[#27272A]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-user"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</span>
</div>
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
{user.name}
</h2>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
<div className="flex flex-col gap-2">
<LoaderButton
loading={loggingOut}
onClick={handleLogout}
className="bg-transparent hover:bg-zinc-900 border border-[#27272A] hover:border-zinc-900"
>
Logout
</LoaderButton>
<LoaderButton loading={deleting} onClick={handleDeleteAccount}>
Delete Account
</LoaderButton>
</div>
{error && (
<div className="border rounded-lg border-[#27272A] px-3 py-2 w-max mt-4">
{error}
</div>
)}
</div>
</div>
</div>
);
};
const LoaderButton = ({ loading, children, ...props }) => {
return (
<button
{...props}
className={`inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-[#0DDD50] text-primary-foreground shadow hover:bg-[#0DDD50]/90 h-9 px-4 py-2 w-full ${props.className} ${loading ? "bg-[#0DDD50]/70 text-white/80" : ""}`}
disabled={loading || props.disabled}
>
{loading && <div className="buttonLoader mr-2"></div>}
{children}
</button>
);
};
const Loader = () => {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black z-50">
<div className="loader"></div>
</div>
);
};
const styles = `
.loader {
display: inline-block;
border: 5px solid rgba(255, 255, 255, 0.6);
border-top-color: rgba(255, 255, 255, 1);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.buttonLoader {
display: inline-block;
border: 3px solid rgba(255, 255, 255, 0.6);
border-top-color: rgba(255, 255, 255, 1);
border-radius: 50%;
width: 18px;
height: 18px;
animation: blspin 1s linear infinite;
}
@keyframes blspin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`;
const injectStyles = () => {
if (typeof document !== "undefined") {
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
}
};
injectStyles();
export default ProfilePage;
8. Protect routes (middleware)
middleware.js
import { NextResponse } from "next/server";
import { jwtVerify } from "jose";
export async function middleware(req) {
const token = req.cookies.get("token")?.value;
const url = req.nextUrl.clone();
console.log("Middleware triggered:", req.nextUrl.pathname);
console.log("Token:", token);
if (url.pathname.startsWith("/profile")) {
console.log("Profile path");
if (!token) {
console.log("No token, redirecting to login");
url.pathname = "/login";
return NextResponse.redirect(url);
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
console.log("Secret:", process.env.JWT_SECRET);
await jwtVerify(token, secret);
console.log("Token verified");
} catch (error) {
console.log("Invalid token, redirecting to login");
url.pathname = "/login";
return NextResponse.redirect(url);
}
} else if (
url.pathname.startsWith("/login") ||
url.pathname.startsWith("/signup")
) {
console.log("Login or signup path");
if (token) {
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
console.log("Secret:", process.env.JWT_SECRET);
await jwtVerify(token, secret);
console.log("Token verified, redirecting to profile");
url.pathname = "/profile";
return NextResponse.redirect(url);
} catch (error) {
console.log("Invalid token, staying on login/signup");
}
}
}
console.log("Next response");
return NextResponse.next();
}
export const config = {
matcher: ["/profile", "/login", "/signup"],
};