Set Up Credentials Authentication with NextAuth in Next.js
NextAuth supports custom email and password authentication using the Credentials Provider. This guide walks you through setting it up in a Next.js app, including secure password handling and database integration. It's ideal if you want full control over your authentication flow.
1. Add the Signin Form and Signup Form UI
You can refer my Signin Form Component and Signup Form Component
In my components, I handled both UI and validation logic, them are useful, you do not care about the Front-end.
2. Setup Database
We need to save user info (name, email, password) on the database, so first you need setup the Database, you can refer my docs.
After setup to connect to MongoAtlas, you need to create User model to save, get user info in DB
Create user interface /types/IUser.d.ts
tsexport interface IUser { firstName: string; lastName: string; email: string; signUpBy: "credentials" | "google" | "facebook" | "github"; password?: string; image?: string; verification?: { code: string; expires: Date; }; }
Create user model /libs/db/models/User.ts
jsimport mongoose, { Model, Schema } from "mongoose"; import { IUser } from "@/types/user"; export interface IUserDocument extends IUser, Document {} const UserSchema: Schema = new Schema<IUserDocument>( { firstName: { type: String, required: true }, lastName: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, signUpBy: { type: String, required: true }, image: { type: String, required: false }, verification: { type: { code: { type: String }, expires: { type: Date }, }, required: false, }, }, { // Need this to virtual _id to id toJSON: { virtuals: true }, toObject: { virtuals: true }, }, ); UserSchema.virtual("id").get(function () { return (this as any)._id.toString(); }); const User: Model<IUserDocument> = mongoose.models.User || mongoose.model<IUserDocument>("User", UserSchema); export default User;
Create user DAO /libs/db/dao/UserDAO.ts
jsimport { IUser } from "@/types/user"; import User from "../models/User"; import dbConnect from "../mongodb"; const getUserById = async (userId: string) => { await dbConnect(); try { return await User.findById(userId); } catch (error) { console.error("Error fetching user:", error); throw new Error("Failed to fetch user by id"); } }; const getUserByEmail = async (email: string) => { await dbConnect(); try { return await User.findOne({ email }); } catch (error) { console.error("Error fetching user:", error); throw new Error("Failed to fetch user by email"); } }; const createUser = async (user: IUser) => { await dbConnect(); try { const existingUser: IUser | null = await User.findOne({ email: user.email, }); if (existingUser) return; await User.create({ firstName: user.firstName, lastName: user.lastName, email: user.email, image: user.image, password: user.password, signUpBy: user.signUpBy, }); return; } catch (error) { console.error("Error creating user:", error); throw new Error("Failed to creating new user"); } }; const UserDAO = { getUserById, getUserByEmail, createUser }; export default UserDAO;
3. Setup API Auth codes
a. Install NextAuth lib
Install NextAuth lib
bashnpm install next-auth or yarn add next-auth
Install bcrypt lib
bashnpm install bcrypt or yarn add bcrypt
b. Setup environment variables
Create a file .evn and add these variables
jsonNEXTAUTH_SECRET="f798f8f2d4d72c3d4062ba69f5xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" NEXTAUTH_DEBUG="true" NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET value you can use any the random string, but I recommend run by command
bashopenssl rand -base64 32
On the production site, you need set NEXTAUTH_DEBUG="false" (because we only use debug for local dev), and NEXTAUTH_URL is your app domain.
After setup values in the .evn file, I ofter create a new file appConfig.ts in the root of folder.
js/* eslint-disable import/no-anonymous-default-export */ export default { // NextAuth NEXTAUTH: { DEBUG: process.env.NEXTAUTH_DEBUG === "true", SECRET: process.env.NEXTAUTH_SECRET, URL: process.env.NEXTAUTH_URL, }, };
c. Setup some constant values
Create a new file /constants/auth.ts
jsexport const SIGN_IN_PROVIDERS = { CREDENTIALS: "credentials", EMAIL: "email", GOOGLE: "google", FACEBOOK: "facebook", GITHUB: "github", } as const;
Create a new file /constants/routes.ts
jsexport const ROUTES = { signIn: "/signin", };
d. Setup NexAuth API
Create a file /app/api/auth/[...nextauth]/route.ts
jsimport NextAuth, { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import bcrypt from "bcryptjs"; import appConfig from "@/appConfig"; import { SIGN_IN_PROVIDERS } from "@/constants/auth"; import { ROUTES } from "@/constants/routes"; import UserDAO from "@/libs/db/dao/UserDAO"; import { IUser } from "@/types/user"; export const authOptions: AuthOptions = { debug: appConfig.NEXTAUTH.DEBUG, secret: appConfig.NEXTAUTH.SECRET, providers: [ CredentialsProvider({ name: "Credentials", // @ts-expect-error: Ignore TS lint async authorize(credentials) { if (!credentials || !credentials.email) return null; const user = await UserDAO.getUserByEmail(credentials.email); if (!user) return null; try { // User sign in by email and verification code (signed in first time by Provider) if (bcrypt.compareSync(credentials?.password || "", user?.password || "")) { return { id: user._id, firstName: user.firstName, lastName: user.lastName, email: user.email, }; // no image } return null; } catch (error) { console.error("Error during authentication by credentials:", error); return null; } }, }), ], pages: { signIn: ROUTES.signIn, // Custom sign-in page (not using the default of NextAuth sign in page) }, session: { strategy: "jwt", maxAge: 60 * 60 * 24, // session will be expired in 1 day, JWT callback will be trigger (in client will call API session for server) }, callbacks: { async jwt({ token, account, user }) { // Run first time when signin // In the second times, the account and user will be undifined if (account?.provider === SIGN_IN_PROVIDERS.CREDENTIALS && user) { token.id = user.id; token.provider = account.provider; token.user = user; // user info from the authorize function above, no need to call user info in DB } return token; }, async session({ session, token }) { session.id = token.id; session.accessToken = token.accessToken as string; session.user = token.user as IUser; return session; }, }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
4. Combine Back-end API with Front-end UI
a. Handle click signin button
In signin form, after user enter all valid information, then user click on the signin button, we add this code below to signin by NextAuth
jsimport { signIn } from "next-auth/react"; ... const res = await signIn(SIGN_IN_PROVIDERS.CREDENTIALS, { email: values.email, password: values.password, callbackUrl, redirect: false, // Prevents full-page reload });
You can refer my full code handleSubmit function
jsconst handleSubmit = async ( values: SigninFormValues, { setFieldValue }: FormikHelpers<SigninFormValues>, ) => { setIsLoading(true); try { const res = await signIn(SIGN_IN_PROVIDERS.CREDENTIALS, { email: values.email, password: values.password, callbackUrl, redirect: false, // Prevents full-page reload }); if (!res || res.error) { openNotification("error", AUTH_MESSAGE.EMAIL_OR_PASSWORD_INVALID); setFieldValue("password", ""); } else { window.location.href = callbackUrl; // Manually navigate after successful sign-in } } catch (error) { console.error("verify password fail***", error); openNotification("error", SYSTEM_MESSAGE.GENERAL_ERROR); } finally { setIsLoading(false); } };
b. Handle click signup button
Create a signup API, create a new file /app/api/auth/signup/route.ts
jsimport { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { SIGN_IN_PROVIDERS } from "@/constants/auth"; import { AUTH_MESSAGE } from "@/constants/sysMessages"; import userDAO from "@/libs/db/dao/UserDAO"; export async function POST(req: NextResponse) { const { firstName, lastName, email, password } = await req.json(); try { const user = await userDAO.getUserByEmail(email); if (user) { return NextResponse.json({ message: AUTH_MESSAGE.USER_EXISTED }, { status: 400 }); } const hashedPassword = await bcrypt.hash(password, 10); await userDAO.createUser({ firstName, lastName, email, password: hashedPassword, signUpBy: SIGN_IN_PROVIDERS.CREDENTIALS, }); // TODO: After save user info into DB, you can send the welcome email to the new user return NextResponse.json({ message: AUTH_MESSAGE.USER_REGISTER_SUCCESS }, { status: 201 }); } catch (error) { console.error(error); return NextResponse.json({ message: AUTH_MESSAGE.USER_REGISTER_FAIL }, { status: 500 }); } }
You can create /constants/sysMessages.ts to save and reuse the messages
jsexport const AUTH_MESSAGE = { USER_SIGNIN_BY_CREDENTIAL: "User sign in by credential", USER_NOT_FOUND: "User not found", USER_REGISTER_SUCCESS: "User registered successfully", USER_REGISTER_FAIL: "User registered fail", USER_EXISTED: "User already exists", CODE_SEND_SUCCESS: "Verification code sent", CODE_SEND_FAIL: "Failed to send email", CODE_EXPIRED: "Code expired", CODE_VERIFIED_SUCCESS: "Code verified successfully", CODE_INVALID: "Invalid code", CODE_VERIFIED_FAIL: "Code verified failed", EMAIL_OR_PASSWORD_INVALID: "Email or password is not correct", };
Create a function to call to the Signup API, create a new file /libs/api/AuthAPI.ts
jsconst signUp = async (firstName: string, lastName: string, email: string, password: string) => { const response = await fetch("/api/auth/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName, lastName, email, password }), }); return await response.json(); }; const AuthAPI = { signUp, }; export default AuthAPI;
In the signup form, after user enter all valid information, then user click on the signup button, we add this code below to call the signup API
jsconst res = await AuthAPI.signUp( values.firstName, values.lastName, values.email, values.password, );
You can refer my full code handleSubmit function
jsconst handleSubmit = async ( values: SignupFormValues, { resetForm, setFieldValue }: FormikHelpers<SignupFormValues>, ) => { setIsLoading(true); try { const res = await AuthAPI.signUp( values.firstName, values.lastName, values.email, values.password, ); if (res.message === AUTH_MESSAGE.USER_REGISTER_SUCCESS) { openNotification("success", AUTH_MESSAGE.USER_REGISTER_SUCCESS); resetForm(); await signIn(SIGN_IN_PROVIDERS.CREDENTIALS, { email: values.email, password: values.password, callbackUrl: ROUTES.homePage, }); } else if (res.message === AUTH_MESSAGE.USER_EXISTED) { openNotification("warning", AUTH_MESSAGE.USER_EXISTED); setFieldValue("password", ""); } } catch (error) { console.error("Sign up failed***", error); openNotification("error", SYSTEM_MESSAGE.GENERAL_ERROR); setFieldValue("password", ""); } finally { setIsLoading(false); } };
Now, we can save the user info into the DB. If user signin, we will get user info in DB then compare with user values that filled on the signin form.
Notes
Because setting up authentication can be quite complicated and involves many steps, if you encounter any errors or need help, feel free to message me via live chat. Thanks!!!