Learn how to implement a Password-Based Next.js login page, in this tutorial.
Vincent
Created: December 30, 2024
Updated: February 17, 2025
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
Next.js is a powerful framework that allows developers to build fast and user-friendly web applications. One of the most critical aspects of any web application is user authentication.
In this guide, we'll walk you through the process of implementing a login page in Next.js with Password-Based Authentication
If you want to see the complete code and check out other authentication methods, use our Next.js login page repository on GitHub. There you will find information regarding:
Recent Articles
♟️
Initial Assessment & Planning (Enterprise Passkeys Guide 1)
♟️
Stakeholder Engagement (Enterprise Passkeys Guide 2)
♟️
Product, Design & Strategy Development (Enterprise Passkeys Guide 3)
♟️
Integrating Passkeys into Enterprise Stack (Enterprise Passkeys Guide 4)
♟️
Testing Passkey Implementations (Enterprise Passkeys Guide 5)
Before we jump into the One-Time Password (OTP) authentication, we need to perform some general project setup steps.
To follow this guide, we require some basic understanding of
Open your terminal and run the following command to create a new Next.js project:
npx create-next-app@latest nextjs-auth-methods
In the installation guide steps, we select the following:
Navigate to your project directory:
cd nextjs-login
To verify that your Next.js project is set up correctly, start the development server:
npm run dev
Open your browser and navigate to http://localhost:3000
. You should see the default Next.js welcome page.
Create a .env.local
file in the root of your project to store environment variables. Add your variables here:
MONGODB_URI=your_database_connection_string GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret TWILIO_ACCOUNT_SID=your_twilio_account_sid TWILIO_AUTH_TOKEN=your_twilio_auth_token TWILIO_PHONE_NUMBER=your_twilio_phone_number
We will guide you step-by-step through creating password-based authentication using Next.js and Tailwind CSS. Whether you're building a new app or enhancing an existing one, you'll learn how to implement sign-up and login features with responsive design.
The following steps are required to implement Password-based authentication:
In this section, we'll dive into the specific files and structure needed for password-based authentication. Here's a clear overview of the relevant directory structure and files:
Key Files and Their Roles:
app/password/login/page.tsx
: Contains the login form component.app/password/signup/page.tsx
: Contains the signup form component.components/AuthForm.tsx
: Reusable form component for login and signup.lib/mongodb.ts
: Sets up the MongoDB connection.models/User.ts
: Defines the user schema.pages/api/auth/password/login.ts
: API route to handle login requests.pages/api/auth/password/register.ts
: API route to handle signup requests.To set up password-based authentication, you need to install the following dependencies:
bcryptjs: This library allows you to hash passwords securely.
mongoose: This library helps you model your data in MongoDB. It provides a straightforward, schema-based solution to model your application data. You can install these dependencies using npm:
npm install bcryptjs mongoose
In this section, we will create a reusable Auth component that will be used for both the login and signup forms. This component will handle the form structure, styling, and state management.
This component will be reused in both the login and signup components, reducing code duplication and making the forms easy to manage and style.
AuthForm
component in the components
directory.AuthForm
component takes mode
, onSubmit
, and resetForm
as props. mode
is used to differentiate between login and signup forms, onSubmit
handles form submission, and resetForm
resets the form fields after submission.useState
to manage email and password input states.handleSubmit
function prevents the default form submission, gathers the input data, and calls the onSubmit
function passed as a prop.Here's the complete code for the AuthForm
component:
"use client"; import { useState, FormEvent, useEffect } from "react"; interface AuthFormProps { mode: "Signup" | "Login"; onSubmit: (data: { email: string, password: string }) => void; resetForm?: boolean; } const AuthForm: React.FC<AuthFormProps> = ({ mode, onSubmit, resetForm }) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); useEffect(() => { if (resetForm) { setEmail(""); setPassword(""); } }, [resetForm]); const handleSubmit = (e: FormEvent) => { e.preventDefault(); onSubmit({ email, password }); }; return ( <form onSubmit={handleSubmit} className="space-y-6"> <h2 className="text-2xl font-bold mb-4 text-center">{mode}</h2> <div> <label className="block text-gray-700 dark:text-gray-300">Email</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="mt-1 p-2 w-full border rounded-md focus:outline-none focus:ring focus:border-blue-300 dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600" /> </div> <div> <label className="block text-gray-700 dark:text-gray-300"> Password </label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="mt-1 p-2 w-full border rounded-md focus:outline-none focus:ring focus:border-blue-300 dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600" /> </div> <button type="submit" className="w-full py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring focus:ring-blue-300" > {mode} </button> </form> ); }; export default AuthForm;
In this section, we will detail how to create the Signup component for user registration. This component will handle user input, submission, and display relevant messages.
Signup
component in the app/password/signup
directory.useState
to manage the message and success states.handleSignup
function manages the form submission, sends a POST request to the server, and updates the state based on the response.Here's the complete code for the Signup component:
"use client"; import { useState } from "react"; import AuthForm from "../../../components/AuthForm"; import Link from "next/link"; const Signup: React.FC = () => { const [message, setMessage] = useState(""); const [isSuccessful, setIsSuccessful] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const handleSignup = async (data: { email: string, password: string }) => { const res = await fetch("/api/auth/password/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); const result = await res.json(); setMessage(result.message); if (res.status === 201) { setIsSuccessful(true); setIsSuccess(true); } else { setIsSuccess(false); } }; return ( <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900"> <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md w-full max-w-md"> {isSuccessful ? ( <> <p className="text-green-500 text-center text-lg font-semibold"> Welcome! </p> </> ) : ( <AuthForm mode="Signup" onSubmit={handleSignup} /> )} {message && ( <p className={`text-center mt-4 ${ isSuccess ? "text-green-500" : "text-red-500" }`} > {message} </p> )} {isSuccessful && ( <Link href="/password/login"> <p className="text-center text-blue-500 font-bold underline py-4"> Back to login </p> </Link> )} </div> </div> ); }; export default Signup;
Next, we will create the Login component for user authentication. This component will handle user input, submission, and display relevant messages.
Login
component in the app/password/login
directory.useState
to manage the message and success states.handleLogin
function manages the form submission, sends a POST request to the server, and updates the state based on the response.Here's the complete code for the Login
component:
import type { NextApiRequest, NextApiResponse } from "next"; import dbConnect from "@/lib/mongodb"; import User from "@/models/User"; import bcrypt from "bcryptjs"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { await dbConnect(); const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } const user = await User.findOne({ email }); if (!user) { return res.status(400).json({ message: "Invalid credentials" }); } const isValidPassword = bcrypt.compareSync(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: "Invalid credentials" }); } return res.status(200).json({ message: "Login successful" }); }
In this section, we will create API endpoints to handle user registration and login requests. The API routes handle incoming HTTP requests for user registration and login.
register.ts
file in the pages/api/auth/password
directory.bcryptjs
.import type { NextApiRequest, NextApiResponse } from "next"; import dbConnect from "@/lib/mongodb"; import User from "@/models/User"; import bcrypt from "bcryptjs"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { await dbConnect(); const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ message: "User already exists" }); } const hashedPassword = bcrypt.hashSync(password, 10); const newUser = new User({ email, password: hashedPassword }); await newUser.save(); return res.status(201).json({ message: "Signup successful!" }); }
login.ts
under src/pages/api/auth/password
directoryimport type { NextApiRequest, NextApiResponse } from "next"; import dbConnect from "@/lib/mongodb"; import User from "@/models/User"; import bcrypt from "bcryptjs"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { await dbConnect(); const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: "Email and password are required" }); } const user = await User.findOne({ email }); if (!user) { return res.status(400).json({ message: "Invalid credentials" }); } const isValidPassword = bcrypt.compareSync(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: "Invalid credentials" }); } return res.status(200).json({ message: "Login successful" }); }
In this section, we will set up a connection to MongoDB, which is crucial for handling user data in our authentication system.
mongodb://localhost:27017
).Create a new database named user_management
.
- Create a new collection within this database named `users`.
Here is a high-level description of the
users
collection relevant for the password-based authentication method:
import mongoose from "mongoose"; const MONGODB_URI: string | undefined = process.env.MONGODB_URI; if (!MONGODB_URI) { throw new Error("Please define the MONGODB_URI environment variable"); } interface MongooseCache { conn: mongoose.Connection | null; promise: Promise<mongoose.Connection> | null; } declare global { var mongoose: MongooseCache; } let cached = global.mongoose; if (!cached) { cached = global.mongoose = { conn: null, promise: null }; } async function dbConnect(): Promise<mongoose.Connection> { if (cached.conn) { return cached.conn; } if (!cached.promise) { const opts = { bufferCommands: false }; cached.promise = mongoose .connect(MONGODB_URI as string, opts) .then((mongoose) => { return mongoose.connection; }); } cached.conn = await cached.promise; return cached.conn; } export default dbConnect;
To avoid TypeScript errors regarding the global cache, add the following to a global.d.ts
file for Global Type Declarations
import mongoose from "mongoose"; declare global { var mongoose: { conn: mongoose.Connection | null; promise: Promise<mongoose.Connection> | null; }; }
In this section, we'll guide you on how to start the application and test the signup and login flows.
Route: http://localhost:3000/password/signup
Steps:
Screenshot of Signup Form:
Route: http://localhost:3000/password/login
Steps:
Screenshot of Login Form:
If the user enters invalid credentials (incorrect email or password), the system provides an error message.
You've successfully implemented a password-based authentication system with Next.js and Tailwind CSS.
Let’s now have a look at the passwordless authentication methods.
Event though we just looked at how to roll traditional Password-Based Authentication, which is not the safest auth method, building a secure and user-friendly authentication system is crucial to protect user data and provide a great first impression of your app. Authentication is often the first interaction a user has with your product and the first thing they will complain about if something goes wrong. Besides the detailed steps laid out above, we also have some additional best practices for Next.js authentication and login pages:
By following these practices, you'll keep your application robust against common threats and ensure a safe environment for your users. Secure authentication not only protects your users but also builds trust and credibility for your application.
In this Next.js login page guide, we explored various authentication methods to secure your Next.js applications. Here’s a recap of what we covered:
TOTP (via authenticator app): We implemented TOTP-based authentication by generating and verifying TOTP secrets, creating QR codes for easy setup, and building front-end components to handle the TOTP process.
Choosing the right authentication method for your application depends on various factors, including security requirements, user convenience, and the nature of your application. Each method we covered has its strengths and can be used alone or in combination to provide a robust authentication system. Experiment with different methods, gather user feedback, and iterate on your implementation to achieve the best balance for your specific needs.
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.