Understanding Concept of Access Token and Refresh Token using JWT in Express.js

Understanding Concept of Access Token and Refresh Token using JWT in Express.js

Introduction

When we talk about web development the things that stike our minds are design of our application, how we will handle the data, what kind of db we will use, how many users will use our application and few others things and then maybe security of our application which is most important part in web development. While working as web developer we should always maintian balance between security and user experience specially during authentication and authorization. One such concept which provides solution to both requirements is access token and refresh token.

Access Token: Temporary token

An access token authenticate's user and provide them temporary access to certain resources, service and functionalities in application. Access token has short life and is designed to be used for limited period of time (minutes or hours). These are often transmitted over insceure channels like http. To reduce the risk of being stolen access tokens are short lived and should be transmitted using secure channels like https.

Refresh Tokens

Now user want to use same application after few hours or say after a day but as we know access token got expired. User have to login again which will be a bad user experience if user wants to use this application every day. Here is when refresh token comes into play. Now user want access to resources which are only given to authenticated user. So, in backend we will check if access token is expired then we will hit other endpoint which will look for refresh token. If refresh token is not expired then we will generate new access token and refresh token to user and user will be automatically authenticated without even knowing this process. Refresh token is generally send in cookies.

Implementing tokens in express.js using jwt

1. Install Required Packages

npm install mongoose express jsonwebtoken

2. Configure MongoDb

/* Create seperate folder named db and file inside it index.js. 
    (db/index.js)
*/
import "mongoose" from mongoose;
const connectDb = async () => {
    try {
        const connectionInstance = await mongoose.connect(
            `${DATABASE_URI}/${DB_NAME}`
        );
        console.log(
            `\n MongoDB Connected !! DB HOST: ${connectionInstance.connection.host}`
        );
    } catch (error) {
        console.log("Mongoose connection error ", error);
        process.exit(1);
    }
};

export default connectDb;

3. Creating Server And Listening Port

import express from "express";
import connectDb from "./db/index.js";

const app = express();

connectDb()
.then(() => {
    const PORT = 8000;
    app.listen(PORT, () => {
        console.log(`Server is running at port: ${PORT}`);
    })
})
.catch((error) => {
    console.log("MongoDb Connection Failed !! ", error);
})

4. Create user model and generate access token and refresh token

import mongoose, {Schema} from "mongoose";
import jwt from "jsonwebtoken";

const userSchema = new mongoose.Schema(
    {
        username: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
            trim: true,
        },
        email: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
        },
        fullname: {
            type: String,
            required: true,
            trim: true,
        },
        refreshtoken: {
            type: String
        }
    },
    {timestamps: true}
)

// create method for generating access token
userSchema.methods.generateAccessToken = function (){
    return jwt.sign(
    {
        _id: this._id,
        username: this.username,
        fullname: this.fullname,
        email: this.email
    },
    'secret_key',
    {expiresIn: '25m'}
  )
}

// create method for generatng refresh token
userSchema.methods.generateRefreshToken = function(){
    return jwt.sign(
        { _id: this._is },
        'Refresh_Secret_Key',
        { expiresIn: refresh_token_expiry }
    )
}

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

5. Logic for updating access token automatically

import {Router} from "express";

const router = Router();

const generateTokens = async (userId) => {
    try {
        // find user from db
        const user = await User.findById(userId);

         // generate new token by calling methods written above
        const accessToken = user.generateAccessToken();
        const refreshToken = user.generateRefreshToken();

        // update refreshToken and save it in db.
        user.refreshToken = refreshToken;
        await user.save({ validateBeforeSave: false });

        return { accessToken, refreshToken };
    } catch (error) {
        throw new ApiError(500, "Something went wrong while generating tokens");
    }
}

router.route("/update-token").post(async (req, res) => {
    // collect refresh token from user and check whether there is access token or not
    const incomingRefreshToken =
        req.cookie.refreshToken || req.body.refreshToken;
    if (!incomingRefreshToken) {
        throw new ApiError(401, "Unauthorized request");
    }

    try {
        //verify token and get it in decoded format
        const decodedToken = jwt.verify(
            incomingRefreshToken,
            process.env.REFRESH_TOKEN_SECRET
        );

        console.log("Decode Token in refresh Endpoint: ", decodedToken);

        //From decoded token we will get user id and we can use this id to find user from mongoDb.
        const user = await User.findById(decodedToken?._id);
        if (!user) {
            throw new ApiError(401, "Invalid refresh token!");
        }
        //Now check whether save user token from db and the token which we get from user is same or not. If not throw error.
        if (incomingRefreshToken !== user?.refreshToken) {
            throw new ApiError(401, "Refresh token is expired or used!");
        }

        //If token is same then generate new access and refresh Token and send response.
        const { newAccessToken, newRefreshToken } = await generateTokens(
            user?._id
        );

        const options = {
            httpOnly: true,
            secure: true,
        };

        res.status(201)
            .cookie("accessToken", newAccessToken, options)
            .cookie("refreshToken", newRefreshToken, options)
            .json(
                new ApiResponse(
                    200,
                    {
                        accessToken: newAccessToken,
                        refreshToken: newRefreshToken,
                    },
                    "Access Token is updated!"
                )
            );
    } catch (error) {
        throw new ApiError(401, error?.message || "Invalid refresh token!");
    }
})

Conclusion

In conclusion, implementing access tokens and refresh tokens in web applications is crucial for balancing security and user experience. Access tokens authenticate users and provide temporary access to resources, while refresh tokens enable seamless authentication without requiring users to re-enter their credentials frequently.

By using JWTs and integrating them into an Express.js application with MongoDB, we can achieve secure and efficient authentication and authorization mechanisms. Access tokens have a short lifespan and are transmitted over secure channels, while refresh tokens, typically stored in cookies, enable automatic token renewal without user intervention.