DEV Community

Rajat Sharma
Rajat Sharma

Posted on

Implementing Credential-Based Authentication with NextAuth.js in Next.js

Implementing Credential-Based Authentication with NextAuth.js in Next.js

Authentication is crucial in controlling user access to web applications. While OAuth providers like Google and GitHub are popular, email/password-based authentication is still one of the most widely-used and secure methods. In this article, we will dive deep into how to implement credential-based authentication using NextAuth.js in a Next.js app. We’ll break down each step clearly, so you can follow along and implement this functionality in your own projects.

What is NextAuth.js?

NextAuth.js is an authentication library designed for Next.js applications. It simplifies handling authentication, including support for third-party providers (OAuth) like Google, GitHub, and Facebook. In this article, however, we’ll focus on using Credential Providers for email and password-based authentication.

What is the Credentials Provider?

The Credentials Provider in NextAuth.js allows you to authenticate users using custom credentials (e.g., email and password), giving you full control over how users log in and how their credentials are validated.

Why use the Credentials Provider?

  • Complete Control: You can define custom logic for login, error handling, and validation (e.g., checking if a user is verified).
  • Simple and Secure: Email/password remains one of the most secure and widely-used methods for authentication.

Let’s walk through the code setup and explain each step.


Step 1: Configure NextAuth.js with the Credentials Provider

The NextAuthOptions configuration object tells NextAuth.js how to handle the authentication process. Below is the full code snippet for setting it up:

import { NextAuthOptions } from "next-auth";
import CredentialProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { dbConnect } from "@/lib/dbConnect";
import UserModel from "@/model/User";

export const authOptions: NextAuthOptions = {
  providers: [
    CredentialProvider({
      id: "credentials", // Unique identifier for the provider
      name: "Credentials", // Display name in the UI
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials: any): Promise<any> {
        await dbConnect(); // Connect to the database

        try {
          const user = await UserModel.findOne({
            $or: [
              { email: credentials.identifier },
              { username: credentials.identifier },
            ],
          });

          if (!user) {
            throw new Error("No user found with this email or username");
          }

          if (!user.isVerified) {
            throw new Error("Please verify your account before logging in");
          }

          const isPasswordCorrect = await bcrypt.compare(credentials.password, user.password);
          if (isPasswordCorrect) {
            return user; // Successfully authenticated
          } else {
            throw new Error("Incorrect password");
          }
        } catch (err: any) {
          throw new Error(err);
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token._id = user._id?.toString();
        token.isVerified = user?.isVerified;
        token.username = user?.username;
        token.isAcceptingMessages = user?.isAcceptingMessages;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.user._id = token._id?.toString();
        session.user.isVerified = token?.isVerified;
        session.user.username = token?.username;
        session.user.isAcceptingMessages = token?.isAcceptingMessages;
      }
      return session;
    },
  },
  pages: {
    signIn: "/sign-in", // Custom sign-in page
    error: "/sign-in",  // Custom error page
  },
  session: {
    strategy: "jwt", // Use JWT for session management
  },
  secret: process.env.NEXTAUTH_SECRET, // Secret key to secure sessions and JWT tokens
};
Enter fullscreen mode Exit fullscreen mode

Explanation of the Code

  • Providers: The Credentials Provider is used here to define the fields needed for login (email and password).

    • id: The unique identifier for the provider (credentials in this case).
    • credentials: The fields required for login (email and password).
    • authorize(): This function handles the login logic. It checks if the user exists, if the account is verified, and whether the entered password matches the one stored in the database.
  • Callbacks:

    • jwt: This callback is called when generating a JWT. We add custom data like the user's ID, username, and verification status.
    • session: This callback is triggered when a session is created or updated. We store additional information from the JWT into the session object.
  • Pages:

    • signIn: The URL path for the custom sign-in page.
    • error: Custom error handling page for authentication failures.

Step 2: Create the API Route to Handle Authentication

Next, we need an API route to handle authentication logic. This is where the actual NextAuth.js authentication mechanism is invoked.

// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import { authOptions } from "@/lib/authOptions"; // Import the authOptions configuration

export default NextAuth(authOptions); // Use the configuration to handle authentication requests
Enter fullscreen mode Exit fullscreen mode

This file simply imports the authOptions from your configuration file and passes it to NextAuth(). The API route will automatically handle requests like logging in, managing sessions, and handling authentication errors.


Step 3: File Structure

To keep things organized, we can structure the project like this:

/lib
  /authOptions.ts        // Configuration for NextAuth.js
/model
  /User.ts               // User model for querying the database
/pages
  /api
    /auth
      [...nextauth].ts    // NextAuth API route
Enter fullscreen mode Exit fullscreen mode

Step 4: How to Handle the Route in Next.js 13+

With the App Router (Next.js 13+), the code should look like this:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import { authOptions } from "@/lib/authOptions"; // Import the configuration

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST }; // Handle both GET and POST requests
Enter fullscreen mode Exit fullscreen mode

This setup allows the route to handle both GET requests (e.g., fetching session info) and POST requests (e.g., logging in).


Conclusion

By following this guide, you should now be able to implement credential-based authentication using NextAuth.js in your Next.js application. With the flexibility of the Credentials Provider, you have full control over the authentication process, from user verification to password comparison.

If you found this tutorial helpful, feel free to share it with others on Dev.to, and don’t hesitate to ask any questions in the comments. Happy coding!


Let me know if you'd like me to modify or add anything further!

Top comments (0)