DEV Community

Oliver Kem
Oliver Kem

Posted on • Updated on

Integrating Credentials Provider with NextAuthJS and MongoDB with Typescript - Part 1

Authentication and Authorization in modern application is very essentials because you are managing user data, but it can be a bit sketchy sometimes as we have to consider route protection, session management, protecting several routes/pages, password encryption, validating user's credentials during sign-up and sign-in.

Next Auth is an open source, free library that handles authentication tasks for the developer like JWT, cookie. It also enables OAUTH2.0 providers like Twitter, GitHub, Google, Facebook and Discord.

Next Auth handles session management and this improves the security of your application by implementing built in security features so that the server can't be tricked easily

In this tutorials, we are going to look at how to implement email password authentication on users' credentials like email and password.

Table Of Contents

    * [Create NextJS App](#chapter-1)
    * [Installing Dependencies](#chapter-2)
    * [User Sign Up](#chapter-3)
    * [Sign In](#chapter-4)
Enter fullscreen mode Exit fullscreen mode

Packages to Install

  • mongoose
  • next-auth
  • bycryptjs

Creating The NextJS Application

I will be using pnpm but be free to use npm if you like. Go to your terminal and run the following line of code

$pnpm create next-app --typescript
Enter fullscreen mode Exit fullscreen mode

We Initialize the application with typescript
Follow the Prompt on the command line to create the NextJS Application. I will not be using the app directory that comes with NextJS 13, I will also be using the src directory with NextJS, but fill free to use it to your own liking.

Installing Dependencies

After successfully creating the nextJS application, we install the dependencies, go to your terminal and run

$pnpm add mongoose next-auth bcryptjs
Enter fullscreen mode Exit fullscreen mode

We will be using a local instance of mongodb server, so we fetch the URL
I will not show how to install mongodb on your pc, feel free to use the database you like
Create a .env.development file in the root folder, then paste the following line of code
MONGODB_URL="mongodb://localhost:27017/test_db"

Model and Controller

NextJS provides us to write API codes in the pages/api folder using the NodeJS environment. It will also follow the same folder-structured route. Create a folder in the api folder and call it auth. Create a route pages/api/auth/sign-up.ts. We also need to make sure that only the POST method is accepted and nothing else.

But before we start working the the route, let's create a user model that we can use to define the components of user
Create a folder called utils. Inside the folder, create a file called user.model.ts. Then paste the following line of code

import mongoose, { Document } from 'mongoose';

const userSchema = new mongoose.Schema({
    name: {
      type: String,
      required: true,
    },
    age: {
      type: Number,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

export interface IUser extends Document {
    name: string;
    age: number;
    email: string;
    password: string;
    createdAt: Date;
    updatedAt: Date;
}
let UserModel: mongoose.Model<IUser>;
try {
  // Try to get the existing model from mongoose
  UserModel = mongoose.model<IUser>('User');
} catch {
  // If the model doesn't exist, define it
  UserModel = mongoose.model<IUser>('User', userSchema);
}
export default UserModel;
Enter fullscreen mode Exit fullscreen mode

Here, we have defined the user model and its contents, we also export the user Interface so that we can use it somewhere else.

We also need to handle sign in to mongodb database. Let's create a file and call it db.ts. Paste in the following line of code

import mongoose from "mongoose";
//connect to mongodb
const connectionUrl = "mongodb://localhost:27017/test_db";
const connectDB = async () => {
    try {
        const conn = await mongoose.connect(process.env.MONGODB_URI||connectionUrl);
        console.log(`MongoDB Connected: ${conn.connection.host}`);
    } catch (err) {
        console.error('Error Encountered: ',err);
        process.exit(1);
    }
};
export default connectDB;
Enter fullscreen mode Exit fullscreen mode

Now lets create a controller file inside the utils folder, name it user.controller.ts. Now lets paste the following code.

import UserModel from './user.model';
type UserX = Omit<IUser,  | 'createdAt' | 'updatedAt' >;
export const createUser = async (user: UserX) => {
    console.log('Request to add user: ', user)
    console.log(await connectDB());
    const userAdded: UserX = {
        ...user,
        password: bcryptjs.hashSync(user.password, 10),
        userType: 'PATIENT',
    }
    console.log('Request to add user: ', userAdded);
    const finduser = await UserModel.findOne({
        email: userAdded.email
    });
    if(finduser) throw new Error("User Found, Change Email");
    const newUser = await UserModel.create(userAdded);
    return newUser;
}
Enter fullscreen mode Exit fullscreen mode

We will use this controller file to sign up to our application. Now back to our sign-up.ts file, we need to handle this authentification, open the file and paste the following line of code


import type { NextApiRequest, NextApiResponse } from 'next';
import { createUser } from '../controllers/user.controller';
type Data = {
  message: string
}

export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
    if (req.method==='POST') {
        const {email, password, ...otherProps} = req.body;
        if (!email || !email.includes('@') || !password) {
            res.status(422).json({ message: 'Invalid Data' });
            return;
        }
        const data = await createUser({email, password, ...otherProps})
        if (!data) {
            res.status(422).json({ message: 'User already exists' });
            return;
        }
        // sign in the user
        res.status(201).json({ message: 'User created',...data });
        return
    }else{
        res.status(500).send({message:'Invalid Route'})
    }
}

Enter fullscreen mode Exit fullscreen mode

We import our user controller file, this is where we will write our API. We destructure the email and password then validate if the user exists, if not we create the user, we then hash the password. If user exists, we throw the error.

Sign In

Now we need to handle the sign in route for the user.

Next-Auth provides us with Client API as well as REST API
The NextAuth.js client library makes it easy to interact with sessions from React applications.

NextAuth.js exposes a REST API which is used by the NextAuth.js client.
We will use both for signing in the users.

To add NextAuth.js to a project create a file called [...nextauth].ts in pages/api/auth.

All requests to /api/auth/* (signin, callback, signout, etc) will automatically be handed by NextAuth.js.

Before we will need to handle the sign in the user, so we should update our controller file

export const getUserByEmail = async (email: string) => {
    const user: UserX | null = await UserModel.findOne({
        email
    });
    if(!user) throw new Error("No User Found");
    return user;
}
Enter fullscreen mode Exit fullscreen mode

With this help from next-auth, we need to implement our own sign-in logic for checking users stored on the database.

In the [...nextauth].ts file, paste in the following lines of code. I will walk you write through it.

import NextAuth, {NextAuthOptions, Session, User} from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { getUserByEmail } from '../controllers/user.controller';
import { IUser } from '../users';
import bcrypt from 'bcryptjs';
import { NextApiHandler } from 'next';
import { getDoctorByStaffID } from '../controllers/doctor.controller';
interface Credentials{
  email: string;
  password: string;
}
export interface MySession extends Session{
    user:{
        id: string,
        name: string,
        email: string,
        phone: string,
        isAdmin: boolean,
        image: string
    }
}
interface MyUser extends User{
    id: string,
    name: string,
    email: string,
    phone: string,
    isAdmin: boolean,
}
export const options: NextAuthOptions = {
    session:{
        strategy: 'jwt',
    },
    callbacks: {
        async jwt({token, user}) {
            if (user) {
                token.user = user;
            }
            return token;
        },
        async session({ session,token }){
            if (token) {
                session.user = token.user as MyUser;
            }
            return Promise.resolve(session);
        },
    },
    secret: process.env.MONGODB_URI,
    providers: [
        Credentials({
            id: 'credentials',
            name: 'credentials',
            credentials:{
                email: {label:'email', type:'text',placeholder:''},
                password:{label:'password',type:'password'}
            },
            async authorize(credentials){
                const { email, password } = credentials as Credentials;
                const user: IUser| null = await getUserByEmail(email);
                if (!user) {
                    throw new Error("No User Found");
                }
                const isMatch = await bcrypt.compare(password, user.password);
                if (!isMatch) {
                    throw new Error("Password doesnt Match");
                }
                return {
                    id: user._id,
                    name: user.name,
                    email: user.email,
                };
            },
        })
    ],
}
const Handler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default Handler;
Enter fullscreen mode Exit fullscreen mode

This code is compatible with NextAuth.js version 4. Here, we import the Credentials provider, this allows us to define a sign in with password and email. Inside the authorize() function we pass in the email and password we had defined as input. we then return the user information. In the callback functions, we have the session() and jwt() function. this takes in the user we had returned in the authorize() so we can define all this. We used type assertions for defining its return type.

In the next session, we will continue with the client side.

Top comments (0)