Australian flagJoin us at the FIDO seminar in Melbourne – Feb 7, 2025!
nextjs passwords bannerEngineering

How to implement Password-Based Authentication in Next.js

Learn how to implement a Password-Based Next.js login page, in this tutorial.

Vincent Delitz

Vincent

Created: December 30, 2024

Updated: January 15, 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.

1. Introduction#

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:

  • Password-based authentication
  • Email OTP (one-time passcode)
  • SMS OTP (one-time passcode)
  • Social login (OAuth)
  • TOTP (via authenticator app)
  • Passkeys
Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

2. Setting Up Your Next.js Project#

Before we jump into the One-Time Password (OTP) authentication, we need to perform some general project setup steps.

2.1 Prerequisites#

To follow this guide, we require some basic understanding of

2.2 Create a New Next.js Project#

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:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes
  • Default import alias: No

Navigate to your project directory:

cd nextjs-login

2.3 Start the Development Server#

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.

2.4 Set Up Environment Variables#

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
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

3. Implementing Password-Based Authentication#

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.

Password-based authentication interface

The following steps are required to implement Password-based authentication:

  1. Set up password-based project
  2. Creating the Auth component
  3. Creating the Signup Component
  4. Creating the Login Component
  5. Setting Up API Routes
  6. Connecting to MongoDB
  7. Testing the Password Authentication

3.1 Set Up Password-Based Project#

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.

3.2 Installing the Relevant Dependencies#

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

3.3 Creating the Auth Component#

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.

  1. File Location: Place the AuthForm component in the components directory.
  2. Purpose: This component will render the form fields (email and password), handle form submission, and display messages.
  3. Explanation
    • Props: The 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.
    • State Management: The component uses useState to manage email and password input states.
    • Form Handling: The 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;

3.4 Creating the Signup Component#

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.

  1. File Location: Place the Signup component in the app/password/signup directory.
  2. Purpose: The Signup component renders the signup form, handles form submission, and displays success or error messages.
  3. Explanation
    • State Management: The component uses useState to manage the message and success states.
    • Form Handling: The 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;

3.5 Creating the Login Component#

Next, we will create the Login component for user authentication. This component will handle user input, submission, and display relevant messages.

  1. File Location: Place the Login component in the app/password/login directory.
  2. Purpose: The Login component renders the login form, handles form submission, and displays success or error messages.
  3. Explanation
    • State Management: The component uses useState to manage the message and success states.
    • Form Handling: The 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" }); }

3.6 Setting Up API Routes#

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.

3.6.1 Register API Route#

  1. File Location: Place the register.ts file in the pages/api/auth/password directory.
  2. Purpose: Handles user registration by receiving email and password, hashing the password, and storing the user data in the database
  3. Explanation:
    • Checks for the presence of email and password.
    • Validates if the user already exists.
    • Hashes the password using bcryptjs.
    • Saves the new user to the database.
    • Returns a success message upon successful registration.
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!" }); }

3.6.2 Login API Route#

  1. File Location: Place the login.ts under src/pages/api/auth/password directory
  2. Purpose: Handles user login by receiving email and password, verifying the user, and checking the password against the stored hash.
  3. Explanation:
    • Checks for the presence of email and password.
    • Validates if the user exists.
    • Compares the provided password with the stored hashed password.
    • Returns a success message if credentials are correct; otherwise, returns an error message.
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" }); }

3.7 Connecting to MongoDB#

In this section, we will set up a connection to MongoDB, which is crucial for handling user data in our authentication system.

3.7.1 Setting Up MongoDB Locally#

  1. Download and Install MongoDB:
    • Go to the MongoDB Server Download
    • Select your operating system and download the MongoDB installer.
    • Follow the installation instructions for your OS.
  2. Install MongoDB Compass:
    • Download MongoDB Compass from the MongoDB website.
    • Install it to easily manage your MongoDB databases visually.
  3. Create a Database and Collection:
    • Open MongoDB Compass and connect to your local MongoDB server (default connection string is mongodb://localhost:27017).

Create a new database named user_management.

MongoDB Compass interface

  • Create a new collection within this database named users. Mongo database creation Here is a high-level description of the users collection relevant for the password-based authentication method: High-level description of users collection

3.7.2 Database Connection File#

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; }; }

3.8 Testing the Authentication Flow#

In this section, we'll guide you on how to start the application and test the signup and login flows.

3.8.1 Testing the Signup Flow#

Route: http://localhost:3000/password/signup

Steps:

  1. Navigate to the signup page.
  2. Enter an email and password.
  3. Click the submit button.
  4. Observe the success or error message.

Screenshot of Signup Form:

Mongo database creation Successful signup interface

3.8.2 Testing the Login Flow#

Route: http://localhost:3000/password/login

Steps:

  1. Navigate to the login page.
  2. Enter the registered email and password.
  3. Click the submit button.
  4. Observe the success or error message.

Screenshot of Login Form:

If the user enters invalid credentials (incorrect email or password), the system provides an error message. Login interface showing invalid credentials error

Password login interface

Successful login interface 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.

4. Recommendation for Next.js Authentication & Login Pages#

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:

  • Don’t Roll out Your Own Auth: Use libraries and packages for tested security solutions and to save implementation time. Libraries like NextAuth.js or Auth0 are excellent choices as they offer robust and secure authentication solutions out of the box.
  • Implement Multi-Factor Authentication (MFA): Combine authentication methods to achieve MFA, e.g., password plus TOTP to add an extra layer of security.
  • Validate Email Addresses: Use APIs to check if email addresses are valid and belong to real users. Avoid allowing one-time or disposable email addresses to prevent spam and ensure genuine user engagement.
  • Prevent SMS Pumping Attacks: Implement rate limits on SMS verifications and restrict access to certain areas to prevent SMS pumping attacks. Use services that can help identify and block fraudulent requests.
  • Limit Login Attempts: To prevent brute force attacks, implement rate limiting on authentication routes. Temporarily block IP addresses after a set number of failed login attempts to protect against automated attacks.
  • Never Store Passwords in Plain Text: Ensure that passwords are stored securely using hashing algorithms like bcrypt. Never store passwords in plain text.
  • Implement Proper Session Management: Authentication is only the first step. After successfully authenticating your users, ensure you have proper session management in place to prevent session hijacking.
  • Use HTTPS Everywhere: Always use HTTPS to encrypt data in transit. This protects user credentials and other sensitive data from being intercepted by attackers.
  • Monitor and Log Authentication Events: Keep track of authentication events and monitor them for suspicious activities. Implement logging and alerting to quickly detect and respond to potential security threats.

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.

5. Conclusion#

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.

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.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free