NextAuth.js Authentication Series — Part 4 ~ Building Email & Password Authentication with Credentials Provider
In the previous article, we integrated Google OAuth authentication using NextAuth.js. While social login is convenient, many applications still require users...

In the previous article, we integrated Google OAuth authentication using NextAuth.js. While social login is convenient, many applications still require users to sign in using an email address and password.
In this article, we'll configure the Credentials Provider, allowing users to authenticate with their own credentials.
Important: This article focuses on configuring the Credentials Provider. We'll use a temporary in-memory user for demonstration purposes. In the next article, we'll connect it to a real database.
What is the Credentials Provider?
Unlike OAuth providers such as Google or GitHub, the Credentials Provider allows you to define your own authentication logic.
This means you are responsible for:
Validating the user's email and password.
Checking whether the user exists.
Verifying the password.
Returning the authenticated user.
The authentication flow looks like this:
User
│
▼
Login Form
│
▼
Credentials Provider
│
▼
Validate Email & Password
│
▼
Return User
│
▼
Create Session
Step 1 — Add the Credentials Provider
Open your lib/auth.ts file.
import CredentialsProvider from "next-auth/providers/credentials";
import type { NextAuthOptions } from "next-auth";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "Email",
type: "email",
},
password: {
label: "Password",
type: "password",
},
},
async authorize(credentials) {
return null;
},
}),
],
};
The authorize() function is the heart of the Credentials Provider. Every login request passes through this function.
Step 2 — Create a Temporary User
Since we haven't connected a database yet, let's create a simple demo user.
const demoUser = {
id: "1",
name: "John Doe",
email: "john@example.com",
password: "123456",
};
This is only for learning purposes.
Never store plain-text passwords in a real application.
Step 3 — Validate User Credentials
Now update the authorize() function.
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
if (
credentials.email === demoUser.email &&
credentials.password === demoUser.password
) {
return {
id: demoUser.id,
name: demoUser.name,
email: demoUser.email,
};
}
return null;
}
If the credentials match, NextAuth creates a session.
Otherwise, authentication fails.
Step 4 — Build a Login Form
Create a client component.
"use client";
import { signIn } from "next-auth/react";
import { useState } from "react";
export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
async function handleLogin(e: React.FormEvent) {
e.preventDefault();
await signIn("credentials", {
email,
password,
redirect: false,
});
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">
Login
</button>
</form>
);
}
Calling signIn("credentials") sends the email and password to the authorize() function.
Step 5 — Understanding the Authentication Process
When the user clicks the login button, the following sequence occurs:
The form submits the email and password.
NextAuth sends the credentials to the Credentials Provider.
The
authorize()function validates the credentials.If validation succeeds, a user object is returned.
NextAuth creates a session.
The user is now authenticated.
If authorize() returns null, the login attempt fails.
Why Are We Returning a User Object?
Many beginners wonder why we return a user object instead of true.
The answer is simple:
NextAuth needs information about the authenticated user so it can create a session.
For example:
return {
id: "1",
name: "John Doe",
email: "john@example.com",
};
This information becomes available in the user's session.
Current Limitations
Our authentication system works, but it still has several limitations:
Passwords are stored in plain text.
Users cannot register.
There is no database.
User data disappears when the server restarts.
We'll solve all of these issues in the upcoming articles.
Best Practices
While learning, avoid these common mistakes:
Never store passwords in plain text.
Always validate input before checking credentials.
Return only the fields you need in the user object.
Never expose sensitive information in the session.
Following these practices early will make your authentication system much more secure.
What's Next?
Our login system currently uses a hardcoded user, which is useful for understanding how the Credentials Provider works but isn't suitable for real-world applications.
In the next article, we'll integrate Prisma with PostgreSQL, create a User model, and authenticate users directly from the database. This will move us one step closer to a production-ready authentication system.


