Credential login with Auth.js (NextAuth)
Authentication is the process of verifying a user's identity. In this tutorial, we'll implement credential-based login - email and password - with Auth.js/NextAuth in a NextAuth.js application.
auth.tsis the authentication configuration fileroute.tscontains the auth endpointproxy.tsis where the routes are protected
Project setup
Accept and clone the template repository for this tutorial.
Install dependencies
There are two new development tools we need to implement authentication:
- next-auth - provides authentication functions
signin(),signout()andauth()for implementing jwt tokens. - bcryptjs - provides hashing functions
The package.json in the template repository
includes dependencies next-auth and bcrypt so they will be included on the
following install command:
npm install
Environment Setup
Copy the .env file you established in previous projects to the project root ( same level
as src ).
We will connect to your established database and add a new collection.
Add an environment variable AUTH_SECRET which is the key used to encode the
JWT and encrypt things in transit. The following command will add the
environment variable to your .env.local.
npx auth secret
AUTH_SECRET is required. It is used to encrypt session cookies and tokens.
JWT Session Strategy
To establish a login session, we'll use JSON web tokens.
auth.config.ts
Configure NextAuth to use jwt as the session strategy:
import { NextAuthConfig } from "next-auth";
export const authConfig: NextAuthConfig = {
session: {
strategy: "jwt",
},
providers: [],
};
When using JWT sessions, Auth.js stores session data in an encrypted cookie.
Auth.js provides the following to implement JWT session cookies:
signIn()- Creates a signed JWT session cookie - logs a user insignOut()- Deletes the cookie - logs a user outauth()- Verifies the JWT on the serverhandlers- Provides the GET and POST route handlers for the auth endpoint- proxy - protect routes
Auth Configuration (auth.ts)
auth.ts is where we define the settings for our authentication.
We set providers and session strategy in this config file.
We set our provider to credentials. This is where you can also add OAuth, etc.
as providers. authorize() looks up the user by email with a
findOne() on the database and if the credentials match a user in the
database we return the user object.
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { authConfig } from "./auth.config";
import connectMongoDB from "./config/mongodb";
import User from "@/app/models/userSchema";
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
CredentialsProvider({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials) return null;
const email = credentials.email as string;
const password = credentials.password as string;
if (!email || !password) return null;
await connectMongoDB();
const user = await User.findOne({ email }).lean();
if (!user || !user.password) return null;
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return null;
return {
id: user._id.toString(),
email: user.email,
name: user.username,
};
},
}),
],
});
The auth.ts file:
handlersprovides theGETandPOSTroute handlers used by the Auth.js API route.authverifies the current session on the server.signInlogs a user in.signOutlogs a user out.CredentialsProvider(...)tells Auth.js that users will authenticate with an email and password.authorize()is where the login credentials are validated.
How authorize() works:
When the user submits the login form, signIn("credentials", ...) calls
authorize(). In authorize():
- The submitted email and password are extracted from
credentials. - The database connection is established.
- The user is looked up by email.
- The submitted password is compared with the hashed password stored in the database using `bcrypt.compare()'.
- If the credentials are valid, a user object is returned.
- If not,
nullis returned.
Returning null tells Auth.js that authentication failed.
Auth Route
The route file:
app/api/auth/[...nextauth]/route.ts
is the main entry point for Auth.js to handle authentication route methods. View this template file:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
This connects the Auth.js route handlers from auth.ts to the /api/auth/[...nextauth] route.
Server Auth Functions
Reusable server-side functions make it easier to log in and log out from components.
doLogout() and doCredentialLogin() are our reusable
server functions for our auth logic. Components like our Navbar which
need to know if the user is logged in can access these.
doLogout()
export async function doLogout() {
await signOut({ redirectTo: "/" });
}
doCredentialLogin
export async function doCredentialLogin(formData:FormData) {
const email = formData.get("email") as string | null;
const password = formData.get("password") as string | null;
try {
await signIn("credentials", {
email,
password,
redirect: false,
});
return { success: true };
} catch (error) {
...
}
What doCredentialLogin() does:
- Reads the email and password from the submitted form
- Calls
signIn("credentials", ...) - Triggers the
authorize()function inauth.ts - Returns success or an appropriate error message
Implement users
We learned from previous projects that to add users to our project means we need a schema to define what the data should look like and we'll need a signup form to add a new user. We'll also need a login form for existing users so we can verify their identity.
user model
To implement authentication, we need to represent users. Each user will be represented as a:
- username
- password
We'll use the email as the unique identifier for each user. The password will be the security measure to verify identity. To implement users, we need a new collection of data. We have learned to create, read, update and delete items. We'll follow a similar path to create and verify users. First, let's create the model for a user.
Create a user schema with a username, email and password (and any other info you want to
include) and construct a User model from the schema. The pasword stored in the database should
be the hashed password, not the plain text password.
import mongoose, { Document, Schema, Model } from "mongoose";
export interface IUser extends Document {
username: string;
email: string;
password: string;
}
const userSchema = new Schema<IUser>({
username: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
});
const User: Model<IUser> = mongoose.models.User || mongoose.model<IUser>("User", userSchema);
export default User;
We need to specify the data type of each field and whether it should be required or not. In this example, every field is required.
- Setting required to true makes the field required for every User model.
- Setting unique to true makes sure no other user has the same username or email.
signup
To create new users, we build a signup form and an API route that hashes the password before storing it.
create a signup form
Create a Signup form client component that takes the user account information as input elements. On the submit of the form, gather the data from the form, make sure required inputs weren't empty and make a fetch POST request with the user data as the body.
const SignupForm = () => {
const router = useRouter();
async function handleSubmit(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
try {
const formData = new FormData(event.currentTarget);
const username = formData.get("username") as string | null;
const email = formData.get("email") as string | null;
const password = formData.get("password") as string | null;
...
const response = await fetch(`/api/signup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
email,
password,
}),
});
...
}
return (
<>
<h1>Signup</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username" >Username</label>
<input type="text" name="username" id="username" required />
</div>
<div className="flex flex-col">
<label htmlFor="email"> Email Address </label>
<input name="email" id="email" required />
</div>
<div>
<label htmlFor="password"> Password </label>
<input type="password" name="password" id="password" required />
</div>
<button type="submit" > Signup </button>
</form>
Already have an account?
...
</>
);
};
export default SignupForm;
create the endpoint for the signup request
In the route.ts file for the signup route, on the POST request, use bcrypt to
hash the password before adding it to the database:
export const POST = async (request:NextRequest ) => {
const {username, email, password} = await request.json();
await connectMongoDB();
const hashedPassword = await bcrypt.hash(password, 5);
const newUser = {
username,
password: hashedPassword,
email
}
try {
await User.create(newUser);
}
}
login
create a login form
Create a LoginForm client component that takes an email and password in
input elements. On the submit of the form, validate the login information
entered to ensure the credentials are valid by passing the data to the
signIn() function provided by auth.js.
const LoginForm = () => {
const router = useRouter();
const [error, setError] = useState<string>("");
async function onSubmit(event: FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
try {
const formData = new FormData(event.currentTarget);
const response = await doCredentialLogin(formData);
}
return (
<div className='ShowItemList'>
<h1>Login</h1>
<form onSubmit={onSubmit}>
<label htmlFor="email"> Email Address</label>
<input type="email" name="email"id="email" placeholder="Email"
required
/>
<label htmlFor="password" > Password </label>
<input type="password" name="password" id="password" placeholder="Password"
required
/>
<button type="submit">
Login
</button>
</form>
Don't you have an account?
...
);
};
export default LoginForm;
Login Flow
- The user submits the login form
doCredentialLogin()is calledsignIn("credentials", ...)runs- Auth.js calls
authorize() - The credentials are checked against the database
- If successful, Auth.js creates the session cookie
- The user is logged in