DEV Community

loading...

Authentication in Web Applications

g4rry420 profile image g4rry420 惻8 min read

Hello Everyone,
In this post, I am going to show you how authentication and login persistance is implemented in web applications using mysql and next.js.I am gonna assume you have some basic knowledge of node and react.

Installation and Folder Structure

Open your favourite code editor and the the following code in the command line:

npx create-next-app hello-next
Enter fullscreen mode Exit fullscreen mode

This will creatte your next application with the name of hello-next.

folder structure

Your folder structure might look different. I recommend you should delete the files and create new files and folder as I have created in the picture.

Next, first we will setup our database which is mysql.

MySQL

If you are following my folder structure then you should have created models folder and in it user.js file.

I have also installed mysql locally in my computer and you can install from this link.
However, you can also setup the cloud for it. The choice is yours.

Now, we are going to create a Schema for our database.
For those of you, who don't know what Schema is, it is structure of our tables in the database about how and which data is going to be stored in the database.Schema is pretty everywhere, it is almost used with every database.

For our mysql database we are going to use sequelize for creating a schema.

npm install --save sequelize
Enter fullscreen mode Exit fullscreen mode

First, we are going to connect sequelize with the database. In the hello-next folder, create a folder called utils and in it create a file called dbConnect.js.

import { Sequelize } from "sequelize";

module.exports = new Sequelize('signup_test', 'root', 'mysql_root_password', {
    host: 'localhost',
    dialect: "mysql"
});
Enter fullscreen mode Exit fullscreen mode

Make to remove mysql_root_password with your root password.

Now, lets move to user.js file. Remember this file is created in the models folder.

const crypto = require("crypto");
const { v4: uuidv1 } = require("uuid")
const { DataTypes } = require("sequelize")

const db = require("../utils/dbConnect")

const User = db.define("User", {
    fullname: {
        type: DataTypes.STRING,
        allowNull: false
    },
    email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
    },
    salt: {
        type: DataTypes.STRING,
        get(){
            return () => this.getDataValue('salt')
        }
    },
    password: {
        type: DataTypes.STRING,
        get(){
            return () =>  this.getDataValue('password')
        }
    }
},{
    tableName: "Users"
})

User.generateSalt = function() {
    return uuidv1()
}

User.encryptPassword = function(plainPassword, salt) {
    return crypto.createHmac('sha256', salt).update(plainPassword).digest("hex");
}

const setSaltAndPassword = user => {
    if(user.changed('password')) {
        user.salt = User.generateSalt();
        user.password = User.encryptPassword(user.password(), user.salt())
    }
}
User.prototype.correctPassword = function(enteredPassword) {
    return User.encryptPassword(enteredPassword, this.salt()) === this.password()
}


User.beforeCreate(setSaltAndPassword);
User.beforeUpdate(setSaltAndPassword);

module.exports = User
Enter fullscreen mode Exit fullscreen mode

You might been seeing an error,we haven't installed the uuid.

npm install uuid
Enter fullscreen mode Exit fullscreen mode

okay, I am gonna walk you real quick about what we are doing in the above file.
We are gonna ask user for the three input values for the signup i.e, fullname, email and password.Now, you might be wondering what is this salt doing in the schema.

Salt

Salt is a value that is added to your cryptography function to make the password of the user encrypted and only salt can decode the password.The point of using salt is that even though we encrypt our password but there are some hackers that can decrypt the password by reverse engineering methods. So, if we add a salt of our choice, it nearly become impossible to crack the password.

Okay, now lets move on
User.generateSalt function is created so that it can generate a unique string of numbers every time when a new user is signuped.

User.encryptPassword = function(plainPassword, salt) {
    return crypto.createHmac('sha256', salt).update(plainPassword).digest("hex");
}
Enter fullscreen mode Exit fullscreen mode

From the naming it is clear that this function receives the plain password and salt encrpt the password.If you want to learn more it, visit this link.

const setSaltAndPassword = user => {
    if(user.changed('password')) {
        user.salt = User.generateSalt();
        user.password = User.encryptPassword(user.password(), user.salt())
    }
}
Enter fullscreen mode Exit fullscreen mode

And now, our final piece of encrpting the password is the setSaltAndPassword which takes the password that the user passed and salt that we generated to encrypt the password.

Remember, we are able to get password using this user.password() method because in our password field we added this get() line.

get(){
            return () =>  this.getDataValue('password')
        }
Enter fullscreen mode Exit fullscreen mode

And the same case goes for the salt also.

Also, for all these functions to take place, we use these methods also. Before, creating any new user data will be passed through them.

User.beforeCreate(setSaltAndPassword);
User.beforeUpdate(setSaltAndPassword);
Enter fullscreen mode Exit fullscreen mode

And finally to check if the password is correct or not, we use correctPassword function. This function encrypts the password passed by the user to check against the encrypt password stored in the database.
By doing this, we will never know about the original password.

API - signup.js, login.js, signout.js and db.js

Remember inside the api, you have to create these files.
Lets, first deal with signup.js file.

const User = require("../../models/user")

export default  (req, res) => {
    User.create(req.body)
        .then(data => {
            return res.json(data)
        })
        .catch(err => {
            if(err){
                return res.status(400).json({
                    error: "Not able to save user in db"
                })
            }
        })
}
Enter fullscreen mode Exit fullscreen mode

Whenever user hits /api/signup/ with post method then new user will be created.
okay, This was simple.

Login.js

const User = require("../../models/user")

const jwt = require("jsonwebtoken")

export default (req, res) => {
    const { email, password } = req.body;


    User.findOne({ where: { email }})
        .then(user => {
            if(!user.correctPassword(password)){
                return res.status(401).json({
                    error: "Email and Password is not correct"
                })
            }

            //create token
            const token = jwt.sign({id: user.id}, process.env.SECRET)

            //send response to front
            const { id, fullname, email } = user;
            return res.status(200).json({
                token, user: { id, fullname, email }
            })

        })
        .catch(err => {
            if(err){
                return res.status(400).json({
                    error: "User email doesn't exist",
                    log: err
                })
            }
        })
}
Enter fullscreen mode Exit fullscreen mode

First things first, npm install jsonwebtoken run this command in your cli.

JsonWebToken (jwt)

Jwt generate a one time unique token generated which will help us to login the user.It maintains the session persistance and we are going to store this token in the cookie so that if a user refreshes the page, he/she is still logged in.
To learn more about the JWT, visit this link.

We are using findOne method of sequelize which find the user based on his email.Remember, in our schema we set the unique: true value for the email so that their will be no duplicates of the email.
Next, we are checking if the password passed by the user is correct or not with correctPassword function.
Next, we are generating a token for the user and sending the user's credentials and token in the frontend.

signout.js

export default (req, res) => {
    res.json({
        message: "remove token and user is signed out"
    })
}
Enter fullscreen mode Exit fullscreen mode

Signout is simple, I am just send the message for now and but in the front end I will remove the cookie so that the user is not signed in.

db.js

import db from "../../utils/dbConnect"


export default function(req, res) {
    db.authenticate()
    .then(res => console.log("DB CONNECTED"))
    .catch(err => console.log("ERROR IN CONNECTING DB"))
    res.json({
        message: "db is connected"
    })
}
Enter fullscreen mode Exit fullscreen mode

This file is to connect database with our application.

signup.js

Okay, moving to frontend, create a signup.js in pages folder.

import React,{ useState } from 'react'
import { useRouter } from "next/router"

export default function signup() {
    const [signup, setSignup] = useState({
        fullname: "",
        email: "",
        password: ""
    })

    const [message, setMessage ] = useState("");
    const router = useRouter();

    const { fullname, email, password } = signup;

    const handleChange = (event) => {
        const { name, value } = event.target;
        setSignup({ ...signup, [name]: value })
    }

    const handleSubmit = (event) => {
        event.preventDefault();

        const data = { fullname, email, password };

        fetch("/api/signup",{
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(data)
        }).then(res => res.json())
          .then(data => {
              if(!data.error){
                  setMessage("Please Login, You are signuped for the app")
              }
          })
          .catch(err => console.log(err))

        setSignup({
            fullname: "",
            email: "",
            password: ""
        })  

        router.push("/login")
    }

    return (
        <form onSubmit={handleSubmit}>
            {message}
            <input type="text" name="fullname" value={fullname} onChange={handleChange} placeholder="Full Name" />
            <br/> <br/>
            <input type="email" name="email" value={email} onChange={handleChange} placeholder='Email' />
            <br/> <br/>
            <input type="password" name="password" value={password} onChange={handleChange} placeholder="Password" />
            <br/> <br/>
            <input type="submit" value="Signup"/>
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

In this component, I am taking the fullname, email and password from the user and then on form submit save data to the /api/signup/ route.

Context

I have setup context in this as it turns out that you cannot do props drilling in next.js.
In the context folder, create a mainContext.js.

import React,{ createContext, useState } from 'react';

export const MainContext = createContext();

export default function MainContextProvider(props) {

    const [authenticated, setAuthenticated] = useState(null);

    return (
        <MainContext.Provider value={{ authenticated, setAuthenticated }}>
            {props.children}
        </MainContext.Provider>
    )
}

Enter fullscreen mode Exit fullscreen mode

Now, in the _app.js, change the code.

import MainContextProvider from "../context/mainContext"
import { CookiesProvider } from "react-cookie"

export default function App({ Component, pageProps }){
    return (
        <MainContextProvider>
            <CookiesProvider>
                <Component {...pageProps} />
            </CookiesProvider>
        </MainContextProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

As you can see we are using react-cookie, which will help us to access the cookie in the application.
So, in the cli, enter npm install react-cookie.

Login.js

Now, create Login.js file in pages folder.

import React,{ useContext, useState } from 'react'
import { useCookies } from "react-cookie"
import { useRouter } from "next/router"
import { MainContext } from '../context/mainContext';

export default function login() {
    const [login, setLogin] = useState({
        email: "",
        password: ""
    });
    const [cookie, setCookie] = useCookies(["token"]);
    const { setAuthenticated } = useContext(MainContext)
    const { email, password } = login;

    const router = useRouter();

    const handleChange = (event) => {
        const { name, value } = event.target;
        setLogin({ ...login, [name]: value })
    }

    const handleSubmit = (event) => {
        event.preventDefault();

        const data = { email, password };

        fetch("/api/login",{
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(data)
        })
        .then(response => response.json()).then(data => {
            if(data.token){
                setCookie("token", `Bearer ${data.token}`, {
                    path: "/",
                    maxAge: 3600, //expires in 1 hour
                    sameSite: true
                })
                if(typeof window !== undefined){
                    localStorage.setItem("jwt", JSON.stringify(data));
                    setAuthenticated(data)
                }
            }
        })
        .catch(err => console.log(err))

        setLogin({
            email: "",
            password: ""
        })
        router.push("/")
    }

    return (
        <form onSubmit={handleSubmit}>
            <input type="email" name="email" value={email} onChange={handleChange} placeholder="Email" />
            <br/> <br/>
            <input type="password" name="password" value={password} onChange={handleChange} placeholder="Password" />
            <br/> <br/>
            <input type="submit" value="Login"/>
        </form>
    )
}

Enter fullscreen mode Exit fullscreen mode

In this component, we are using react-cookie which gives us the use cookies hooks in which returns three things.

  1. cookie:- The cookie which is curently stored in your browser.
  2. setCookie:- To store a new cookie in the browser.
  3. removeCookie:- To remove the cookie from the browser.

Although, in this component, we are using the first two only but in the index.js you will see the example of removeCookie.

Furthermore, I am taking the email and password as a input from the user and on form submit, checking the email and password with /api/login route.
In the success response from it, I got the token and user credentials in which I store the token using the setCookie which take some options like maxAge which describes when the cookie is gonna expires which in this case is 1 hour.

Next, I am also storing the data in the localStorage and in the autenticated state.

Finally, when the success is done, router is push backed to "/".

Index.js

This is the main component on which the application mounts on.

import { useEffect, Fragment, useContext } from "react"
import { useCookies } from "react-cookie"

import Link from "next/link"
import { MainContext } from "../context/mainContext";

export default function Home() {

  const { authenticated, setAuthenticated } = useContext(MainContext);

  const [cookie, setCookie, removeCookie] = useCookies(["token"]);


  useEffect(() => {
    dbCall()
  }, [])

  useEffect(() => {
    if(!authenticated){ 
      setAuthenticated(isAuthenticated())
    }
  },[authenticated])

  const dbCall = async () => {
    const response = await fetch("/api/db")
    const data = await response.json();
    console.log(data)
  }

  const handleSignout = () => {
    fetch("/api/signout").then(response => response.json())
    .then(data => {
      if(data.message === "remove token and user is signed out"){
        removeCookie("token")
        setAuthenticated(null)
      }
    })
    .catch(err => console.log(err))
  }

  const isAuthenticated = () => {
    if(typeof window !== undefined){
      if(localStorage.getItem("jwt") && cookie.token){
        return JSON.parse(localStorage.getItem("jwt"));

      }else{
        return false
      }
    }
  }

  return (
    <ul>
      <li><Link href="/">Home</Link></li>
      {
        authenticated && authenticated.user ? (
          <li onClick={handleSignout}>Signout</li>
        ) : (
          <Fragment>
            <li><Link href="/login">Login</Link></li>
            <li><Link href="/signup">SignUp</Link></li>
          </Fragment>
        )
      }
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this component, there are two main functions isAuthenticated and handleSignout.

isAuthenticated() :- This function is invoked when the app is mounted for the first and it checks if the jwt key for the localStorage and cookie token is presented in it and then it will authenticate the user and setAuthenticated state is updated.Based on this, login, signup and signout is gonna be the present to the user.

handleSignout() :- When the user clicked on signout, we remove the token cookie and setAuthenticated state to null.

Also, in this component, we are connecting our database with dbCall function.

Conclusion

Now, you have the authenticated application with user persistance session.

If you think I might have mentioned something wrong, please feel free to comment. We all are learning here.

Thanks for your time to read this.
Happy Coding:)

Discussion (0)

Forem Open with the Forem app