roxxhub

How to create your own authentication system in MongoDB ?

Why you should make authentication system yourself ?

While there are plenty of popular services (like Firebase Authentication, Auth0 etc.) which provide you a way to let users do signin / login which just a few lines of code, in my opinion , making your own authentication is much better option. The reason being same as why you would code your own site, instead of using a cms like wordpress or some website builder. And that is you want flexibility and don’t want to rely completely on a service.

Plus it is not wise handing your valuable data such as user data etc. to a third party service. YOU WANT EVERY DATA IN YOUR OWN DATABASE.

Why use mongodb ?

And that is where mongodb comes into play. Its fast, flexible (it doesn’t restrict users to use same field for every entry, unlike sql) , and easy to use. The download part is not covered in this post.

Steps of creating an auth system

  1. Letting people create their account using few credentials (in our case its just name and password, you can have more, like email or something).
  2. We store the users name and hashed password in our database. (We hash the password, instead of storing the password string directly. This ensures even if our database has been breached, attackers still wont get the password directly. You can hash other information of user too if you want. And for hashing we use the module bcryptjs).
  3. We also need a login system.
  4. When a person logs in or signs in, we would also like to send a token. This token would get stored in users browser. Every time the user opens the site, this token , if exists would be used to check to authenticate the user. This omits the need of user to login every time they visit the site. We use the jsonwebtoken module to do so.

NOTE: THE FOLLWONG CODE IS WRITTEN FOR NEXTJS13.4( JAVASCRIPT ). THE SYNTAX HAS TO BE CHANGED ACCORDING THE LANGUAGE YOU ARE USING

Important modules to download

  1. mongoose
  2. bcryptjs
  3. jsonwebtoken

1. SIGNIN ENDPOINT / API

In nextjs 13.4 before doing anything, we first need to create a user model.

const mongoose = require("mongoose");
mongoose.pluralize(false)

const userSchema = new mongoose.Schema({
    name : {
        type: String, 
        required: true,
        unique: true
    },

    passwd : {
        type: String,
        required: true
    },

   }
);

export const users = mongoose.models.users || mongoose.model("users", userSchema)

A schema is like a rule / filter, which defines the collection you are using. Here each document of our collection has 2 fields. Name – a string, that should be unique, and password. You can have more fields.

NOTE: MAKE SURE TO MAKE A NEW DATABASE FIRST. LIKE IN OUR CASE THE DATABASE NAME IS DB_002. IF YOU DONT HAVE THIS COLLECTION (users) THERE ALREADY, IT WILL BE AUTOMATICALLY GENERATED.

Then we use this model in the page where we need to signin.

import { NextResponse } from "next/server"
import { users } from "@/lib/models/users"
import mongoose from "mongoose"
import bcrypt from 'bcryptjs'
const jwt = require("jsonwebtoken")

export const  POST = async(Request) =>{
    try{
    // 1
    const uri = 'mongodb://127.0.0.1/db_002'        // uri = mongodb://127.0.0.1/your_db_name
    await mongoose.connect(uri) 

    const req = await Request.json()
    // 2

    const name = await users.findOne({ name: req.name })
    if(name){
        return NextResponse.json({mssg: 'name already used',success: false})
    }

    //  3
    const secpass = await bcrypt.hash(req.passwd, 10)  
    const user = await users.create({
        name: req.name,
        passwd: secpass,
      })
    
   // 4
    const data = {id: user.id, name: user.name}
    const jwt_token = process.env.JWTSEC
    const token = jwt.sign(data, jwt_token)

    return NextResponse.json({token: token, success: true})}
    catch{
        return NextResponse.json({"error": 'some internal error occucred'})

    }
}

So in the step no. 1 in the try wrap, we are connecting to our database (db_002). We are also defining the request json as ‘req’

When a user sends the credentials, we would first like to see if the username has been already used or not. So that’s our step 2. If the name is not unique, we send them a message that user by this name already exists and the code doesn’t executes further

If the user name is unique, in step 3, we simply hash our password, using bcrypt. (notice that the second argument 10 refers to the size of randomly generated salt you want. A salt is like an addition string added to the raw password string before hashing it out. This salt is unique for everyone. This salt is stored along with the stored hash password. This bigger you make the number, the bigger the salt gets. Bigger salts result in a more secure password, but 10 is sufficient.)

And finally we create a new user using users.create function.

In step 4, we use the id and name of our user to create token and send it to the user. (This token would be convertible into the the original json (id and name) later).

LOGIN ENDPOINT / API

import { NextResponse } from "next/server"
import mongoose from "mongoose"
import bcrypt from 'bcryptjs'
import { users } from "@/lib/models/users"
const jwt = require("jsonwebtoken")

export async function POST(Request) {
   // 1
    try{const uri = `${process.env.DB_URI}`
    await mongoose.connect(uri)
    const req = await Request.json()

//2
    let user = await users.findOne({ name: req.name })
    if (!user) {
        return NextResponse.json({ mssg: 'wrong creds', sucsess: false })
    }
    const allowd = await bcrypt.compare(req.passwd, user.passwd)
    if (!allowd) {
        return NextResponse.json({ mssg: "wrong creds", success: false })
    }
//3
    const data = { "id": user.id, "name": user.name }
    const jwt_token = process.env.JWTSEC
    const usertoken = jwt.sign(data, jwt_token)
    return NextResponse.json({ "token": usertoken, success: true })}
    catch{
        return NextResponse.json({"error": 'some internal error occucred'})
    }

}

Step 1 is same as step 1 in signin step.

in step 2, we are checking if a user by the given name exists or not, and then comparing the password with the stored hashed password(note: the salt is automatically applied) using the compare function. It return true or false.

Finally if every credential is correct, we send the token to the user.

FETCHING USER USING TOKEN ENDPOINT / API

import { NextResponse } from "next/server"
import { headers } from "next/headers"
const jwt = require("jsonwebtoken")
import mongoose from "mongoose"
import { users } from "@/lib/models/users"

export const POST = async (Request) => {
    try {
//1
    const uri = `${process.env.DB_URI}`
    await mongoose.connect(uri)
//2
    const token = headers().get("auth-token");
    const jwt_token = process.env.JWTSEC
    if (!token) {
        return NextResponse.json({ error: "please enter a token" })
    }
    try {
        const data = jwt.verify(token, jwt_token);
        const userdata = await users.findById(data.id).select("-passwd")
    return NextResponse.json(userdata)
    }
    catch {
        return NextResponse.json({ "error": "please enter a valid token" })
    }

    
    }
    catch{
        return NextResponse.json("some internal error occurred")
    }

}

step 1 is as usual , connecting to the database.

in step 2, we are checking the header of the request . If any header named auth-token exists, we convert it into the json it initially was (which included id and and name of the user) and then fetch out the user document (everything except password), and send it to user.