User Authentication and Authorization using JSON Web Token(JWT) in Node.js

·

5 min read

JSON Web Token(JWT) is an open standard used to share security information between two parties i.e. client and server.

JSON stands for JavaScript Object Notation and is a text-based format for transmitting data across web applications.Token is a string of data that represents identity(in case of JWT).

Each JWT contains encoded JSON object,including a set of claims.Claims are used to transmit information between two parties. Claim may contain who issued the token, how long it is valid for, or what permissions the client has been granted.

A JWT consist of three parts separated by dot(.) and serialized using base64. It looks similar to xxxxx.yyyyy.zzzzz .

Once decoded it consists of two JSON strings:

  • header and payload
  • signature

header contains the type of token and the signing algorithm.payload contains the claims which is the information that the server uses to verify if the user has the permission to do the operation they are requesting.

The signature ensures that the token hasn’t been altered. The party that creates the JWT signs the header and payload with a secret that is known to both the issuer and receiver, or with a private key known only to the sender. When the token is used, the receiving party verifies that the header and payload match the signature.

To authenticate and authorize user using JWT , we need to first create access token and refresh token and send them to the user in the HTTP response header after they login with a registered email and password. For creating access token and refresh token we first create a secret word for access token and refresh token and sign the userId of the logged in user with the secret word. We send the access token in the header of the HTTP response and refresh token as a cookie because they provide a way to store data that lasts across the duration of a session or throughout browser tabs.

const {sign}=require('jsonwebtoken')


const createAccessToken=(userId)=>{
    return sign({userId},process.env.ACCESS_TOKEN_SECRET,{
        expiresIn:'20m'
    })
}

const createRefreshToken=(userId)=>{
    return sign({userId},process.env.REFRESH_TOKEN_SECRET,{
        expiresIn:'7d'
    })
}

const sendAccessToken=(req,res,accesstoken)=>{
    res.send({
        accesstoken,
        email:req.body.email
    })
}

const sendRefreshToken=(res,refreshtoken)=>{
    res.cookie('refreshtoken',refreshtoken,{
        httpOnly:true,
        path:'/refresh_token'
    })
}

Endpoint for login of the user: Email and password is obtained from request body.Users are stored in MongoDB so existence of the user is first checked and the password is compared. If the user is valid then access token and refresh token is created using the user Id which is the id with which it is stored in the database.The refresh token for the user stored in database is then updated and the access token and refresh token are sent in the HTTP response header and cookie respectively.

app.post("/login", async (req, res) => {
  const { email, password } = req.body;
  try {
    const user = await User.findOne({ email: email });
    if (!user) throw new Error("User does not exist");

    const valid = await compare(password, user.password);
    if (!valid) {
      throw new Error("Incorrect password");
    } else if (valid) {
      const refreshToken = createRefreshToken(user._id);
      const accessToken = createAccessToken(user._id);
      User.findByIdAndUpdate(
        { _id: user._id },
        { $set: { token: refreshToken } },
        { new: true, useFindAndModify: false }
      ).then(() => {
        sendRefreshToken(res, refreshToken);
        sendAccessToken(req, res, accessToken);
      });
    }
  } catch (err) {
    res.send({ error: `${err.message}` });
  }
});

Endpoint for updating the refresh token:Refresh token is needed so that user isn't automatically logged out when the access token and expires or user refreshes the browser. First token is extracted from the cookies and then verified with the refresh token secret which was used earlier to create refresh token in the first place.If the token doesn't match then the empty access token is sent in the response header due to which the user wont be able to request for any operation and will be logged out and if the token matches then new refresh token and access token will be created and be sent as well as updated with the one in the database.

app.post("/refresh_token", async (req, res) => {
  const token = req.cookies.refreshtoken;
  if (!token) return res.send({ accesstoken: "" });
  let payload = null;
  try {
    payload = await verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (error) {
    return res.send({ accesstoken: "" });
  }
  //if token valid check if user exists
  const user = await User.findOne({ _id: payload.userId });

  if (!user) return res.send({ accesstoken: "" });
  if (user.token !== token) {
    return res.send({ accesstoken: "" });
  }
  const accesstoken = createAccessToken(user._id);
  const refreshtoken = createRefreshToken(user._id);
  //update with new refresh token and send accesstoken and refreshtoken
  User.findByIdAndUpdate(
    { _id: user._id },
    { $set: { token: refreshtoken } },
    { new: true, useFindAndModify: false }
  ).then(() => {
    sendRefreshToken(res, refreshtoken);
    return res.send({ accesstoken });
  });
});

Endpoint for logout of the user:Upon logout request , refresh token value contained in the cookie is cleared which in turn clears the access token as well and user is logged out from performing any further operation. useEffect checks in the first render for the refresh token and if exists replaces the access token with a new access token so that user doesn't automatically gets logged out.A loading variable needs to be used in order to give some time for the end point to bring back the access token and update its value or else on re-render the user will be logged out before checking for the refresh token.

app.post("/logout", (req, res) => {
  res.clearCookie("refreshtoken", { path: "/refresh_token" });
  return res.send({
    message: "Logged out",
  });
});
//frontend part
  useEffect(() => {
    const checkRefreshToken = async () => {
      const result = await (
        await fetch("https://app.com/refresh_token", {
          method: "POST",
          credentials: "include",
          headers: {
            "Content-Type": "application/json",
          },
        })
      ).json();

      setUser({
        accessToken: result.accesstoken,
      });
       setLoading(false);
    };
    checkRefreshToken();
  }, []);