DEV Community

Whoissosick
Whoissosick

Posted on

How to use JWTokens properly

User registration

First thing first we need to register a user

  1. Destucturing the received username & password from request.
  2. Check for duplicate username in the db
  3. Encrypt the password using encrypt package
  4. Create and save new user into db
  5. Send proper status and make error handling
const { username, password } = req.body; 
  if (!username || !password) return res.status(400).json({ "message": 'Username and password are required.'});

  const duplicate = await User.findOne({ username: username }).exec();
  if (duplicate) return res.sendStatus(409); // Conflict

  try {
    // encrypt the password 
    const hashedPassword = await bcrypt.hash(password, 10); 

    // create and store the new user
    const result = await User.create({ 
      "username": username, 
      "password": hashedPassword 
    }); 

    res.status(201).json({ 'success': `New user ${username} created`});
  } catch (err) {
    res.status(500).json({ "message": err.message });
  }
Enter fullscreen mode Exit fullscreen mode

User authorization

After we registrate user, we also need to get username/password from request.body and cookies (content from it)

  1. Find user by username in db 1.1 If user is not found => 401 | Unathorized
  2. Compare password using bcrypt package
  3. Create AT (Access Token) & RT (Refresh Token). Tokens contains (payload, SECRET, options).
  4. If user came in without RT inside cookies, array of RTs stays unchanged, if cookies contains something we save array without that RT. (This supports multiple devices).
  5. Clear all cookie before we give new tokens
  6. Update array of tokens inside db
  7. User gets AT & RT
  const cookies = req.cookies; 
  const { username, password } = req.body;
  if (!username || !password) return res.status(400).json({ "message": 'Username and password are required' });

  const foundUser = await User.findOne({ username: username }).exec();
  if (!foundUser) return res.sendStatus(401);

  const matchPassword = await bcrypt.compare(password, foundUser.password);
  if (matchPassword) {
    const roles = Object.values(foundUser.roles);

    const accessToken = jwt.sign(
      {
        "UserInfo": {
          "username": foundUser.username,
          "roles": roles
        }
      },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: '30s' }
    );
    const newRefreshToken = jwt.sign(
      { "username": foundUser.username },
      process.env.REFRESH_TOKEN_SECRET,
      { expiresIn: '1d' }
    );

    const newRefreshTokenArray = 
      !cookies?.jwt
        ? foundUser.refreshToken
        : foundUser.refreshToken.filter(rt => rt !== cookies.jwt);
    if (cookies?.jwt) {
      res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true });
    }

    foundUser.refreshToken = [...newRefreshTokenArray, newRefreshToken];
    const result = await foundUser.save();

    res.cookie('jwt', newRefreshToken, { httpOnly: true, sameSite: 'None', maxAge: 24 * 60 * 60 * 1000 });
    res.json({ accessToken });
  } else {
    res.sendStatus(401);
  }
Enter fullscreen mode Exit fullscreen mode

Update AT by RT

  1. Clear user cookie
  2. Find user by RT in db
  3. If user is not found in db, user is hacked. We intentionally clear array of tokens.
  4. Create new array without received RT from cookie
  5. Verify RT and give new tokens, then save it into db
  6. Send cookie and result
const cookies = req.cookies;
  if (!cookies?.jwt) return res.sendStatus(401);

  const refreshToken = cookies.jwt;
  res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true });

  const foundUser = await User.findOne({ refreshToken: refreshToken }).exec();

  if (!foundUser) {
    jwt.verify(
      refreshToken,
      process.env.REFRESH_TOKEN_SECRET,
      async (err, decoded) => {
        if (err) return res.sendStatus(403);
        const hackedUser = await User.findOne({ username: decoded.username }).exec();
        hackedUser.refreshToken = [];
        const result = await hackedUser.save();
        console.log(result);
      }
    );
    return res.sendStatus(403) // Forbidden
  };

  const newRefreshTokenArray = foundUser.refreshToken.filter(rt => rt !== refreshToken);

  jwt.verify(
    refreshToken,
    process.env.REFRESH_TOKEN_SECRET,
    async (err, decoded) => {
      if (err) { // if RT has been expired 
        foundUser.refreshToken = [...newRefreshTokenArray];
        const result = await foundUser.save();
        return res.sendStatus(403);
      }
      if (err || foundUser.username !== decoded.username) return res.sendStatus(403);

      const roles = Object.values(foundUser.roles);

      const accessToken = jwt.sign(
        {
          "UserInfo": {
            "username": decoded.username,
            "roles": roles
          }
        },
        process.env.ACCESS_TOKEN_SECRET,
        { expiresIn: '30s' },
      );

      const newRefreshToken = jwt.sign(
        { "username": foundUser.username },
        process.env.REFRESH_TOKEN_SECRET,
        { expiresIn: '1d' }
      );

      foundUser.refreshToken = [...newRefreshTokenArray, newRefreshToken];
      const result = await foundUser.save();

      res.cookie('jwt', newRefreshToken, { httpOnly: true, sameSite: 'None', maxAge: 24 * 60 * 60 * 1000 });
      res.json({ accessToken });
    }
  );
Enter fullscreen mode Exit fullscreen mode

User logout

  1. Get RT from cookies
  2. Find user by RT inside db. (if user is not found, clear cookie)
  3. Delete RT in db
  4. Clear cookie
  const cookies = req.cookies; 
  if (!cookies?.jwt) return res.sendStatus(204); 

  const refreshToken = cookies.jwt; 

  const foundUser = await User.findOne({refreshToken: refreshToken}).exec(); 
  if (!foundUser) {
    res.clearCookie('jwt', { 
      httpOnly: true, 
      sameSite: 'None', 
      secure: true 
    });
    return res.sendStatus(204);
  }

  foundUser.refreshToken = foundUser.refreshToken.filter(rt => rt !== refreshToken);
  const result = await foundUser.save();
  console.log(result);

  res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true }); // secure: true - only serves on https (it's on production)
  res.sendStatus(204);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)