DEV Community

Cover image for JWT Authentication with Access Tokens & Refresh Tokens In Node JS
CyberWolves
CyberWolves

Posted on

JWT Authentication with Access Tokens & Refresh Tokens In Node JS

What's up guys. We all know most important feature in every application is authentication. To make that authentication much more secured and make better user experience we need to use refresh and access token based authentication in your app. You might be thinking what is refresh token?, why should we use?, how should we use ?. Well don't worry I'm gonna cover everything from scratch.

So let's start coding...

I highly recommend you to watch demo video for better understanding. If you like my work Subscribe to my channel to support.

Demo Video

Project Github Link

Following table shows the overview of Rest APIs that exported

Methods Urls Actions
POST /signUp Signup User
POST /logIn Login User
POST /refreshToken Get new access token
DELETE /refreshToken Logout User

What is refresh token?

A refresh token is nothing but a access token but it has life time about 1 or 2 months. access token has expire time about 10 to 15 minutes. when ever this access token expire. we don't ask user to login again to get new access token instead we send refresh token to the server here we verify that token and send new access token to the client. with this method user don't have to login again and again. this makes user experience much more easier to user.

create Node.js App

$ mkdir refreshTokenAuth
$ cd refreshTokenAuth
$ npm init --yes
$ npm install express mongoose jsonwebtoken dotenv bcrypt joi joi-password-complexity 
$ npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode

Project Structure

Project Structure

package.json

{
  "name": "refreshTokenAuth",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^5.0.1",
    "dotenv": "^16.0.0",
    "express": "^4.17.3",
    "joi": "^17.6.0",
    "joi-password-complexity": "^5.1.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.2.8"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}
Enter fullscreen mode Exit fullscreen mode

User Model
/models/User.js

import mongoose from "mongoose";

const Schema = mongoose.Schema;

const userSchema = new Schema({
    userName: {
        type: String,
        required: true,
    },
    email: {
        type: String,
        required: true,
        unique: true,
    },
    password: {
        type: String,
        required: true,
    },
    roles: {
        type: [String],
        enum: ["user", "admin", "super_admin"],
        default: ["user"],
    },
});

const User = mongoose.model("User", userSchema);

export default User;
Enter fullscreen mode Exit fullscreen mode

User Token Model
/models/UserToken.js

import mongoose from "mongoose";

const Schema = mongoose.Schema;

const userTokenSchema = new Schema({
    userId: { type: Schema.Types.ObjectId, required: true },
    token: { type: String, required: true },
    createdAt: { type: Date, default: Date.now, expires: 30 * 86400 }, // 30 days
});

const UserToken = mongoose.model("UserToken", userTokenSchema);

export default UserToken;
Enter fullscreen mode Exit fullscreen mode

Generate Tokens Function
/utils/generateTokens.js

import jwt from "jsonwebtoken";
import UserToken from "../models/UserToken.js";

const generateTokens = async (user) => {
    try {
        const payload = { _id: user._id, roles: user.roles };
        const accessToken = jwt.sign(
            payload,
            process.env.ACCESS_TOKEN_PRIVATE_KEY,
            { expiresIn: "14m" }
        );
        const refreshToken = jwt.sign(
            payload,
            process.env.REFRESH_TOKEN_PRIVATE_KEY,
            { expiresIn: "30d" }
        );

        const userToken = await UserToken.findOne({ userId: user._id });
        if (userToken) await userToken.remove();

        await new UserToken({ userId: user._id, token: refreshToken }).save();
        return Promise.resolve({ accessToken, refreshToken });
    } catch (err) {
        return Promise.reject(err);
    }
};

export default generateTokens;
Enter fullscreen mode Exit fullscreen mode

Verify Refresh Token Function
/utils/verifyRefreshToken.js

import UserToken from "../models/UserToken.js";
import jwt from "jsonwebtoken";

const verifyRefreshToken = (refreshToken) => {
    const privateKey = process.env.REFRESH_TOKEN_PRIVATE_KEY;

    return new Promise((resolve, reject) => {
        UserToken.findOne({ token: refreshToken }, (err, doc) => {
            if (!doc)
                return reject({ error: true, message: "Invalid refresh token" });

            jwt.verify(refreshToken, privateKey, (err, tokenDetails) => {
                if (err)
                    return reject({ error: true, message: "Invalid refresh token" });
                resolve({
                    tokenDetails,
                    error: false,
                    message: "Valid refresh token",
                });
            });
        });
    });
};

export default verifyRefreshToken;
Enter fullscreen mode Exit fullscreen mode

Validation Schema Function
/utils/validationSchema.js

import Joi from "joi";
import passwordComplexity from "joi-password-complexity";

const signUpBodyValidation = (body) => {
    const schema = Joi.object({
        userName: Joi.string().required().label("User Name"),
        email: Joi.string().email().required().label("Email"),
        password: passwordComplexity().required().label("Password"),
    });
    return schema.validate(body);
};

const logInBodyValidation = (body) => {
    const schema = Joi.object({
        email: Joi.string().email().required().label("Email"),
        password: Joi.string().required().label("Password"),
    });
    return schema.validate(body);
};

const refreshTokenBodyValidation = (body) => {
    const schema = Joi.object({
        refreshToken: Joi.string().required().label("Refresh Token"),
    });
    return schema.validate(body);
};

export {
    signUpBodyValidation,
    logInBodyValidation,
    refreshTokenBodyValidation,
};
Enter fullscreen mode Exit fullscreen mode

Auth Routes
/routes/auth.js

import { Router } from "express";
import User from "../models/User.js";
import bcrypt from "bcrypt";
import generateTokens from "../utils/generateTokens.js";
import {
    signUpBodyValidation,
    logInBodyValidation,
} from "../utils/validationSchema.js";

const router = Router();

// signup
router.post("/signUp", async (req, res) => {
    try {
        const { error } = signUpBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const user = await User.findOne({ email: req.body.email });
        if (user)
            return res
                .status(400)
                .json({ error: true, message: "User with given email already exist" });

        const salt = await bcrypt.genSalt(Number(process.env.SALT));
        const hashPassword = await bcrypt.hash(req.body.password, salt);

        await new User({ ...req.body, password: hashPassword }).save();

        res
            .status(201)
            .json({ error: false, message: "Account created sucessfully" });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

// login
router.post("/logIn", async (req, res) => {
    try {
        const { error } = logInBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const user = await User.findOne({ email: req.body.email });
        if (!user)
            return res
                .status(401)
                .json({ error: true, message: "Invalid email or password" });

        const verifiedPassword = await bcrypt.compare(
            req.body.password,
            user.password
        );
        if (!verifiedPassword)
            return res
                .status(401)
                .json({ error: true, message: "Invalid email or password" });

        const { accessToken, refreshToken } = await generateTokens(user);

        res.status(200).json({
            error: false,
            accessToken,
            refreshToken,
            message: "Logged in sucessfully",
        });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Refresh Token Routes
/routes/refreshToken.js

import { Router } from "express";
import UserToken from "../models/UserToken.js";
import jwt from "jsonwebtoken";
import { refreshTokenBodyValidation } from "../utils/validationSchema.js";
import verifyRefreshToken from "../utils/verifyRefreshToken.js";

const router = Router();

// get new access token
router.post("/", async (req, res) => {
    const { error } = refreshTokenBodyValidation(req.body);
    if (error)
        return res
            .status(400)
            .json({ error: true, message: error.details[0].message });

    verifyRefreshToken(req.body.refreshToken)
        .then(({ tokenDetails }) => {
            const payload = { _id: tokenDetails._id, roles: tokenDetails.roles };
            const accessToken = jwt.sign(
                payload,
                process.env.ACCESS_TOKEN_PRIVATE_KEY,
                { expiresIn: "14m" }
            );
            res.status(200).json({
                error: false,
                accessToken,
                message: "Access token created successfully",
            });
        })
        .catch((err) => res.status(400).json(err));
});

// logout
router.delete("/", async (req, res) => {
    try {
        const { error } = refreshTokenBodyValidation(req.body);
        if (error)
            return res
                .status(400)
                .json({ error: true, message: error.details[0].message });

        const userToken = await UserToken.findOne({ token: req.body.refreshToken });
        if (!userToken)
            return res
                .status(200)
                .json({ error: false, message: "Logged Out Sucessfully" });

        await userToken.remove();
        res.status(200).json({ error: false, message: "Logged Out Sucessfully" });
    } catch (err) {
        console.log(err);
        res.status(500).json({ error: true, message: "Internal Server Error" });
    }
});

export default router;
Enter fullscreen mode Exit fullscreen mode

.env file
/.env

DB = Your database URL
SALT = 10
ACCESS_TOKEN_PRIVATE_KEY = Add your private key
REFRESH_TOKEN_PRIVATE_KEY = Add your private key
Enter fullscreen mode Exit fullscreen mode

Database Connect
/dbConnect.js

import mongoose from "mongoose";

const dbConnect = () => {
    const connectionParams = { useNewUrlParser: true };
    mongoose.connect(process.env.DB, connectionParams);

    mongoose.connection.on("connected", () => {
        console.log("Connected to database sucessfully");
    });

    mongoose.connection.on("error", (err) => {
        console.log("Error while connecting to database :" + err);
    });

    mongoose.connection.on("disconnected", () => {
        console.log("Mongodb connection disconnected");
    });
};

export default dbConnect;
Enter fullscreen mode Exit fullscreen mode

Sever.js
/server.js

import express from "express";
import { config } from "dotenv";
import dbConnect from "./dbConnect.js";
import authRoutes from "./routes/auth.js";
import refreshTokenRoutes from "./routes/refreshToken.js";

const app = express();

config();
dbConnect();

app.use(express.json());

app.use("/api", authRoutes);
app.use("/api/refreshToken", refreshTokenRoutes);

const port = process.env.PORT || 8080;
app.listen(port, () => console.log(`Listening on port ${port}...`));
Enter fullscreen mode Exit fullscreen mode

That's it guys we have successfully implemented refresh and access token based authentication in Node JS.

For bonus within this project I have implemented routes which only authenticated users can access and role based authorization. You can find it in Demo Video

Thank You :)

Top comments (12)

Collapse
 
farzindev profile image
Farzin • Edited

NOT SECURE AT ALL.
You are sending Refresh token with response and you are expecting the refresh token from body? You need to understand that this method is basically the worst way of doing it. So many things are wrong with your codes, but most important thing:
You should send the refresh token as httpOnly secure cookie with proper sameSite.

res.cookie('jwt', newRefreshToken, {
    httpOnly: true, 
    secure: true,
    sameSite: 'Strict',  // or 'Lax', it depends
    maxAge: 604800000,  // 7 days
});
Enter fullscreen mode Exit fullscreen mode

And when you want to get a new access token, inside your refresh controller you get the refresh token from cookie (req.cookies.jwt) and then verify it.

Dear author, you can't just write some article about this topics if you are not professional at it.

Collapse
 
volodymyrmatselyukh profile image
volodymyr-matselyukh

Totally agree with Farzin. In your solution refresh token decreases security of the system? Refresh token can be compromised with the same probability as access token (because they both reside in the same place - body) and at the same time refresh token has a way longer lifetime. That's absolutely decreases security.

Collapse
 
mavericx profile image
Mavericx

clearly, you @farzindev and @volodymyrmatselyukh see lots of problems in this implementation. why don't you guys write better implementations addressing those issues?

still, @cyberwolves 's article is a good reference for those unaware of how to handle such cases.

Collapse
 
knightndgale profile image
Mark-Dave-S

chill man :v

Collapse
 
sebelga profile image
Sébastien Loix

He is right. Security is a serious topic... only experts in the field should be teaching it with actual best practices.

Thread Thread
 
knightndgale profile image
Mark-Dave-S

yeah I know man, you gotta chill or this site will become the next stackoverflow

Collapse
 
mohamad_el_bohsaly profile image
Mohamad El Bohsaly

Thank you @cyberwolves for your comprehensive documentation.
Let me get this straight:

  1. Login or Sign up generates a new access token accompanied with a refresh token
  2. Upon firing protected API calls, I use the access token inside the verifyToken middleware function. In case access token got expired, I use the refresh token instead and regenerate an access token
  3. Logging out removes both tokens
Collapse
 
hermesfire profile image
Hermes-fire

Can you make an article about the frontend part using react

Collapse
 
dcarapic profile image
Dalibor Čarapić

If I understood correctly you can not be logged in on multiple devices because there is only one refresh token per user?

Collapse
 
hongphuc5497 profile image
Hong Phuc

It's a different aspect, refreshToken only helps you secure the application more properly compare to using 1 accessToken. If you want to restrict one user per device, you need to save users' info whenever they log in and confront it with newer info.

Collapse
 
arya011tp profile image
Arya Aniket

what is the purpure of refresh Token, when you have to have only accessToken and you store refresh token

Collapse
 
q_d_cd009cfa5ff99c45f714b profile image
Q D

Refresh token is used to obtain a new access token (short lived) when it is expired instead of using user/password. Refresh token has a longer life time.