When working with Next.js, a React-based framework, it's common to encounter scenarios where secure access to certain application pages is required. NextAuth is an open-source authentication solution specifically designed for Next.js applications. It offers built-in features for authentication, making it a go-to choice for most web applications. If you're building your project with Next.js, integrating NextAuth is a smart move to ensure application security.
What We'll Use:
* Next.js 15 with TypeScript
* NextAuth v4
* MongoDB Atlas (Database)
* Tailwind CSS (Styling)
Step 1: Create Your Project
Start by creating a new Next.js project using the following command:
npx create-next-app@latest
Step 2: Install Required Packages
Next, install the necessary dependencies:
npm install next-auth axios bcryptjs form-data jsonwebtoken mongoose
Types of Authentication with NextAuth
NextAuth supports two main types of authentication:
1. Providers (e.g., Google, GitHub, Facebook, etc.)
2. JWT (JSON Web Tokens)
* JWT is suitable for simple authentication using a username and password.
* Providers allow you to integrate social platform logins like Google, making it easier for users to sign in.
Step 3: Define Authentication Types in next-auth.d.ts (For typescript)
Create a next-auth.d.ts file to define the user authentication types:
import"next-auth";
declare module"next-auth" {
interfaceUser {
_id: string;
name: string;
picture: string;
email: string;
username: string;
access_token: string;
refresh_token: string;
expires_on: number;
exp: number;
iat: number;
jti: string;
}
interfaceSessionextendsDefaultSession {
user: User;
expires_in: string;
error: string;
}
interfaceProfile {
email_verified?: boolean;
}
}
Step 4: Create Pages
For demonstration purposes, we’ll create the following pages:
Frontend Pages
1. Signup: signup/page.tsx
'use client'
import Link from "next/link";
import { useState } from "react";
interface formType {
username: string;
password: string;
}
export default function Page() {
const [loading, setLoading] = useState(false)
const [form, setForm] = useState<formType>({ username: '', password: '' });
const handleChange = (e: { target: { name: string, value: string } }) => {
setForm({
...form,
[e.target.name]: e.target.value
})
}
const handleSubmit = async (e: React.FormEvent<HTMLButtonElement>) => {
e.preventDefault();
setLoading(true)
const url = '/api/signup';
const formdata = new FormData();
formdata.append('username', form.username);
formdata.append('password', form.password);
const response = await fetch(url, {
method: 'POST',
body: formdata,
}).then(res => res.json())
setLoading(false)
alert(response.result)
}
return (
<section className="text-gray-600 body-font">
<Link href={'/'} className="flex text-white m-4 absolute right-4 rounded-full bg-indigo-600 p-2 px-4">Login</Link>
<div className="container py-24 flex flex-wrap items-center justify-center mx-auto">
<div className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col w-full mt-10 md:mt-0">
<h2 className="text-gray-900 text-lg font-bold title-font mb-5">Create an account</h2>
<div className="relative mb-4">
<label htmlFor="full-name" className="leading-7 text-sm text-gray-600">Username</label>
<input
onChange={handleChange}
type="text"
id="username"
name="username"
className="w-full bg-white rounded border border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" />
</div>
<div className="relative mb-4">
<label htmlFor="email" className="leading-7 text-sm text-gray-600">Password</label>
<input
onChange={handleChange}
type="password"
id="password"
name="password"
className="w-full bg-white rounded border border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" />
</div>
<button
onClick={handleSubmit}
className="text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded text-lg">
{loading?<>Loading...</>:<>Signup</>}
</button>
</div>
</div>
</section>
)
}
2. Login: login/page.tsx
'use client'
import Link from "next/link";
import { useState } from "react";
import { useSession } from "next-auth/react";
import { signIn } from "next-auth/react";
import { redirect } from "next/navigation";
import googleSigninPng from '@/app/images/google.png'
// import fbSigninPng from '@/app/images/fb.png'
import Image from "next/image";
interface formType {
username: string;
password: string;
}
export default function Page() {
const session = useSession();
const [loading, setLoading] = useState(false)
if (session.data) {
redirect('/profile')
}
const [form, setForm] = useState<formType>({ username: '', password: '' });
const handleChange = (e: { target: { name: string, value: string } }) => {
setForm({
...form,
[e.target.name]: e.target.value
})
}
const handleSubmit = async (e: React.FormEvent<HTMLButtonElement>) => {
setLoading(true)
e.preventDefault();
const res = await signIn('credentials', {
redirect: false,
username: form.username,
password: form.password,
});
setLoading(false)
console.log(res)
if (res?.status === 200) {
return redirect('/profile')
}
else {
alert('Invalid credentials')
}
}
return (
<>
<section className="text-gray-600 body-font">
<Link href={'/signup'} className="flex text-white m-4 absolute right-4 rounded-full bg-indigo-600 p-2 px-4">Signup</Link>
<div className="container py-24 flex flex-col flex-wrap items-center justify-center mx-auto">
<div className="lg:w-2/6 md:w-1/2 bg-gray-100 rounded-lg p-8 flex flex-col w-full mt-10 md:mt-0">
<h2 className="text-gray-900 text-lg font-bold title-font mb-5">User Login</h2>
<div className="relative mb-4">
<label htmlFor="full-name" className="leading-7 text-sm text-gray-600">Username</label>
<input
onChange={handleChange}
type="text"
id="username"
name="username"
className="w-full bg-white rounded border border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" />
</div>
<div className="relative mb-4">
<label htmlFor="email" className="leading-7 text-sm text-gray-600">Password</label>
<input
onChange={handleChange}
type="password"
id="password"
name="password"
className="w-full bg-white rounded border border-gray-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" />
</div>
<button
onClick={handleSubmit}
className="text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded text-lg">
{loading ? <>Loading...</> : <>Login</>}
</button>
</div>
<br></br>
<small>OR</small>
<br></br>
<div className="flex items-center gap-2">
<button
onClick={() => signIn("google")}
className="text-white focus:outline-none rounded text-lg">
<Image src={googleSigninPng} alt="google" width={200} />
</button>
{/* <button
onClick={() => signIn("facebook")}
className="text-white focus:outline-none rounded text-lg border">
<Image src={fbSigninPng} alt="facebook" width={240} />
</button> */}
</div>
</div>
</section>
</>
);
}
3. Profile: profile/page.tsx
'use client'
import { useSession, signOut } from "next-auth/react";
import { redirect } from "next/navigation";
export default function LoginStatusButton() {
const session = useSession();
console.log(session);
if (!session.data) {
redirect('/')
}
if (session.data !== null) {
return (
<section className="text-gray-600 body-font">
<div className="container px-5 py-24 mx-auto flex items-center md:flex-row flex-col">
<div className="flex flex-col md:pr-10 md:mb-0 mb-6 pr-0 w-full md:w-auto md:text-left text-center">
<h1 className="text-6xl font-medium title-font text-gray-900">Welcome</h1>
<h2 className="text-xs text-indigo-500 tracking-widest font-bold title-font mb-1 p-2">
You are logged in as - {session?.data?.user?.username || session?.data?.user?.email}
</h2>
</div>
<div className="flex md:ml-auto md:mr-0 mx-auto items-center flex-shrink-0 space-x-4">
<button className="bg-indigo-500 text-white inline-flex py-3 px-5 rounded-lg items-center hover:bg-indigo-600 focus:outline-none">
<span className="flex items-start flex-col leading-none">
<span className="title-font font-medium" onClick={()=>signOut()}>Sign out</span>
</span>
</button>
</div>
</div>
</section>
)
}
}
API Routes
1. Signup Route: Handles user registration.
import { NextResponse } from "next/server";
import { userData } from '@/database/user';
import bcrypt from 'bcryptjs';
export async function POST(req: Request) {
try {
// Parse the incoming FormData
const formData = await req.formData();
const username = formData.get('username') as string;
const password = formData.get('password') as string;
if (username === '' || password === '') {
return NextResponse.json({ message: 'error', result: 'Invalid credentials' }, { status: 400 });
}
if (password.length < 4) {
return NextResponse.json({ message: 'error', result: 'Please Choose Strong Password' }, { status: 400 });
}
const isAvailable = await userData.findOne({ Username: username });
if (!isAvailable) {
const encPass = await bcrypt.hash(password, 10);
const newUser = new userData({
Username: username,
Password: encPass,
})
await newUser.save();
return NextResponse.json({ message: 'success', result: 'Account created successfully' }, { status: 200 });
} else {
return NextResponse.json({ message: 'error', result: 'user already exist' }, { status: 400 });
}
} catch (error) {
console.error('Error parsing FormData:', error);
return NextResponse.json({ message: 'Failed to process form data' }, { status: 400 });
}
}
2. Login Route: Handles user login.
import { NextResponse } from "next/server";
import { userData } from "@/database/user";
// import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
export async function POST(req: Request) {
const data = await req.formData();
const username = data.get('username') as string;
const password = data.get('password') as string;
if (username === '' || password === '') {
return NextResponse.json({ message: 'error', result: 'Invalid credentials' }, { status: 400 });
}
const user = await userData.findOne({ Username: username });
if (user) {
const isPassValid = await bcrypt.compare(password, user.Password)
if (!isPassValid) {
return NextResponse.json({ message: 'error', result: 'Invalid user' }, { status: 400 })
}
// const token = jwt.sign({
// id: user._id,
// username: user.Username
// },
// process.env.JWT_SECRET || '',
// {
// expiresIn: '2h',
// });
return NextResponse.json({ message: 'success', username: user.Username }, { status: 200 })
} else {
return NextResponse.json({ message: 'error', result: 'User not found' }, { status: 400 })
}
}
3. NextAuth Route: auth/[...nextauth]/route.ts (ensure the folder name is exactly as shown).
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { NextAuthOptions } from "next-auth";
import FormData from "form-data";
import axios from "axios";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID || "",
clientSecret: process.env.FACEBOOK_CLIENT_SECRET || ""
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const data = {
username: credentials?.username,
password: credentials?.password,
};
const form = new FormData();
form.append('username', data.username)
form.append('password', data.password)
try {
const res = await axios.post(`${process.env.API_URL}/login`, form)
const resData = await res.data;
console.log(resData);
if (resData) {
return resData;
} else {
// console.error('Authorization failed:', resData);
return null;
}
} catch (error) {
console.error('Authorization error:', error);
return null;
}
}
})
],
pages: {
signIn: '/login'
},
secret: process.env.JWT_SECRET,
session: { strategy: "jwt" },
callbacks: {
async signIn({ account, profile }) {
if (account?.provider === "google" || account?.provider === 'facebook') {
return profile?.email_verified ?? false;
}
return true;
},
async jwt({ token, user }) {
return { ...token, ...user }
},
async session({ session, token }) {
session.user = {
email: token.email as string,
name: token.name as string,
picture: token.picture as string,
id: token.id as string,
_id: token._id as string,
username: token.username as string,
access_token: token.access_token as string,
refresh_token: token.refresh_token as string,
expires_on: token.expires_on as number,
exp: token.exp as number,
iat: token.iat as number,
jti: token.jti as string,
};
return session;
}
}
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Ensure all API routes are defined properly, and filenames end with .ts and not .tsx.
API Routes Overview
Signup Route: Handles user registration.
Login Route: Handles authentication with credentials.
NextAuth Route: Handles provider-based authentication.
Step 5: Authentication with Providers (Google console setup)
For this example, we’ll use Google as the authentication provider. You’ll need a Client ID and Client Secret from Google. Follow these steps to get the credentials:
1. Log in to the Google Cloud Console.
2. Create a new project.
3. Navigate to APIs & Services → OAuth Consent Screen.
4. Choose External and configure the consent screen.
5. Go to Credentials → Create Credentials → OAuth Client ID.
6. Add JavaScript Origins and Redirect URIs.
JavaScript Origins
* http://localhost:3000
* https://yourdomain.com
Redirect URIs
* http://localhost:3000/api/auth/callback/google
* https://yourdomain.com/api/auth/callback/google
With this setup, you’re ready to implement secure authentication in your Next.js 15 application using NextAuth. Whether you're opting for JWT or provider-based authentication, NextAuth simplifies the process and ensures robust security for your project. Happy coding!