Setting up Next.js with NextAuth, Prisma and "Credentials" Auth Provider
Tags: nextjs, typescript, nextauth, prisma
Published: 2023-12-25 11:13:43

Authentication is a fundamental part of most web applications. Integrating authentication into your Next.js app can be simplified with NextAuth, a powerful authentication library that supports various authentication methods. However, the documentation around setting up NextAuth with the "Credentials" auth provider might not be as clear as you'd hope.
My implementation is greatly enriched and partially based on [Next-Auth docs](https://next-auth.js.org/providers/credentials) and the following [github thread](https://github.com/nextauthjs/next-auth/discussions/4394).

### Understanding NextAuth and the "Credentials" Provider

NextAuth simplifies the authentication process by providing a flexible and easy-to-use API. It supports numerous authentication providers, including social logins (like Google, Facebook, etc.) and custom authentication mechanisms through the "Credentials" provider.


### Enhancing Types for NextAuth Sessions and Users
Before we start coding, managing our types is crucial for any TypeScript-based applications, especially when working with NextAuth. The following updated next-auth.d.ts file enriches the existing types provided by NextAuth, extending the session and user interfaces to include additional properties that are required by my app.

```
import { Role } from "@prisma/client";
import NextAuth, { DefaultUser } from "next-auth";

declare module "next-auth" {
  interface Session {
    user?: DefaultUser & {
      id: string;
      role: Role;
    };
  }
  interface User extends DefaultUser {
    role: Role;
  }
}
```

The Session interface now extends the default user object with the properties id and role, providing more specific typing for these attributes within sessions. Similarly, the User interface extends the DefaultUser interface by adding the role property from the Role. These enhancements offer a more comprehensive typing structure for NextAuth sessions and users, ensuring stronger type safety throughout our authentication workflow.

### Exploring the API Code
The code provided here contains the necessary configurations and functions to set up NextAuth with the "Credentials" provider. 

It's essential to highlight that while this implementation allows for user sign-up using credentials, it's highly recommended to incorporate email verification as part of the sign-up process. 

Ensuring email verification adds an extra layer of security and credibility to user accounts, deterring potential misuse. However, discussing the specifics of implementing email verification goes beyond the scope of this post but remains an important consideration for enhancing account security.

```
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import type { NextAuthOptions, SessionOptions } from "next-auth";
import { encode, decode } from "next-auth/jwt";
import type { Adapter } from "next-auth/adapters";

import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";

import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/lib/prisma";

import bcrypt from "bcrypt";
import { randomUUID } from "crypto";

import Cookies from "cookies";

const getAdapter = (req: NextApiRequest, res: NextApiResponse): Adapter => ({
  ...PrismaAdapter(prisma),
  async getSessionAndUser(sessionToken) {
    const userAndSession = await prisma.session.findUnique({
      where: { sessionToken },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            emailVerified: true,
            name: true,
            image: true,
            role: true,
          },
        },
      },
    });
    
    if (!userAndSession) return null;

    const { user, ...session } = userAndSession;

    return { user, session };
  },
});

const session: SessionOptions = {
  strategy: "database",
  maxAge: 30 * 24 * 60 * 60, // 30 days
  updateAge: 24 * 60 * 60, // 24 hours
  generateSessionToken: async () => randomUUID(),
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const adapter = getAdapter(req, res);
  const authOptions: NextAuthOptions = {
    providers: [
      CredentialsProvider({
        name: "Credentials",
        credentials: {
          name: { label: "Name", type: "name", placeholder: "Name" },
          email: { label: "Email", type: "email", placeholder: "Email" },
          password: { label: "Password", type: "password" },
        },
        async authorize(credentials, req) {
          if (!credentials) return null;
          const { name, email, password } = credentials;

           let user = await prisma.user.findUnique({ where: { email } });
          if (!user && !!name && !!email && !!password) {
            // sign up 
            const hashedPassword = await bcrypt.hash(password, 10);
            user = await prisma.user.create({
              data: { email, password: hashedPassword },
            });
          } else {
            user = await prisma.user.findUnique({ where: { email } });
            if (
              !user ||
              !user.password ||
              !bcrypt.compareSync(password, user.password)
            )
              return null;
          }
          return {
            id: user.id,
            name: user.name,
            email: user.email,
            image: user.image,
            role: user.role,
          };
        },
      }),
    ],
    adapter,
    callbacks: {
      async session({ session, user }) {
        if (session.user) {
          session.user.id = user.id;
          session.user.role = user.role;
        }
        return session;
      },
      async signIn({ user }) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          if (user && "id" in user) {
            const sessionToken = randomUUID();
            const sessionExpiry = new Date(Date.now() + session.maxAge! * 1000);

            if (!adapter.createSession) return false;
            await adapter.createSession({
              sessionToken,
              userId: user.id,
              expires: sessionExpiry,
            });

            const cookies = new Cookies(req, res);
            cookies.set("next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
            });
          }
        }

        return true;
      },
    },
    jwt: {
      maxAge: session.maxAge,
      async encode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          const cookies = new Cookies(req, res);
          const cookie = cookies.get("next-auth.session-token");

          if (cookie) return cookie;
          else return "";
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return encode(params);
      },
      async decode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          return null;
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return decode(params);
      },
    },
    pages: {
      signIn: "/account/signin",
    },
    session,
    cookies: {
      sessionToken: {
        name: "next-auth.session-token",
        options: {
          httpOnly: true,
          sameSite: "lax",
          path: "/",
          secure: process.env.NODE_ENV === "production",
        },
      },
    },
    debug: process.env.NODE_ENV === "development",
  };

  return NextAuth(req, res, authOptions);
}
```

Here's a breakdown of the key sections:

#### Defining the Adapter and Session Configuration
The getAdapter function returns a Prisma adapter for handling sessions and users. The session object defines the session configuration, including the strategy and session token generation.

#### Creating the NextAuth Options
The authOptions object holds the configuration for NextAuth, including providers, callbacks for handling sessions and sign-ins, JWT configuration, page routes, and cookie settings.

#### Customizing Authentication Logic
The "Credentials" provider's authorize function handles user authentication based on provided credentials (name, email, password). It hashes passwords and interacts with the Prisma database to create or retrieve users.

### Improving Clarity and Documentation
While the code above provides a comprehensive setup for NextAuth with the "Credentials" provider, clarity in documentation is essential for developers. A more elaborate breakdown of each function, its purpose, and the flow of data between them could greatly benefit developers trying to set up this authentication flow.

### Conclusion
Setting up Next.js with NextAuth and the "Credentials" provider allows for a flexible and customizable authentication system within your application. While the official documentation might lack clarity in some areas, breaking down the code and providing context can help developers understand the integration better.

Remember, authentication is a critical aspect of your application's security. Always ensure to handle sensitive user data securely and follow best practices when implementing authentication mechanisms.

In conclusion, with NextAuth and its "Credentials" provider, you can create a robust authentication system tailored to your application's needs.