authenticationjwtmernexpress-jwt

Can a JSON web Token payload be updated on the server-side?


I was working on a project where users can create concerts with a front end built using React and a back end built using Express.js.

I used jsonwebtoken and express-jwt to generate tokens when my users logs in and I set it up so that when the token gets generated it will add the concerts associated with the current user as well so I can show them on the front end on the user's profile.

Here's my User schema:

const { Schema, model, SchemaTypes } = require("mongoose");

const userSchema = new Schema(
  {
    username: {
      type: String,
      required: [true, "Username is required."],
      unique: true,
    },

    email: {
      type: String,
      required: [true, "Email is required."],
      unique: true,
      lowercase: true,
      trim: true,
    },
    password: {
      type: String,
      required: [true, "Password is required."],
    },

    image: {
      type: String
    },
    concert: {
      type: [SchemaTypes.ObjectId],
      ref: 'Concert',
      default:[]
    }
    
  },
  {
    timestamps: true,
  }
);

module.exports = model("User", userSchema);

And here's my login route

router.post("/login", (req, res, next) => {
  const { username, password } = req.body;

  // Check if username or password are provided as empty string
  if (username === "" || password === "") {
    res.status(400).json({ message: "Provide email and password." });
    return;
  }

  // Check the users collection if a user with the same username exists
  User.findOne({ username })
  .populate("concert")
    .then((foundUser) => {
      if (!foundUser) {
        // If the user is not found, send an error response
        res.status(401).json({ message: "User not found." });
        return;
      }

      // Compare the provided password with the one saved in the database
      const passwordCorrect = bcrypt.compareSync(password, foundUser.password);

      if (passwordCorrect) {
        // Deconstruct the user object to omit the password
        const { _id, username, image, concert } = foundUser;

        // Create an object that will be set as the token payload
        const payload = { _id, username, image, concert };

        // Create a JSON Web Token and sign it
        const authToken = jwt.sign(payload, process.env.TOKEN_SECRET, {
          algorithm: "HS256",
          expiresIn: "6h",
        });

        
        res.status(200).json({ authToken: authToken }); // Send the token as the response
      } else {
        res.status(401).json({ message: "Unable to authenticate the user" });
      }
    })
    .catch((err) => next(err));
});

After a user logs in, the token is stored inside localStorage and I'm using React Context API to retrieve the user data and share it throughout the app.

The issue I was having was that after logging in if a user created a new concert, it wouldn't appear on their profile because the current token only held the concert data which existed at the time of the login.

So I needed to find a way to update the token after a concert was created so their profile would update accordingly.

I googled and read the documentation for the jsonwebtoken package but there was no example or tutorial that addressed what I was looking for.

So I came up with a workaround where I generate a new token after a concert is created on the server then I send that token to the front end so it can be used to "replace" the token I had when the user logged in.

Here's what the code for the server looks like

const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Concert =  require('../models/Concert.model');
const User = require('../models/User.model');
const { isAuthenticated } = require("../middleware/jwt.middleware.js");
const jwt = require("jsonwebtoken");


router.post('/concerts', isAuthenticated, (req, res) => {
  const { title, image, description,  country, city, street, houseNumber, postalCode, comment } = req.body;
  const userId = req.payload._id

  Concert.create({ title, image, description, country, city, street, houseNumber, postalCode, comment: [] }) 
  .then(newConcert => {
      return User.findByIdAndUpdate(userId, {
        $push: { concert: newConcert._id },
      },{new: true})
      .populate("concert")
    })
      .then((updatedUser) => {
        const {_id, username, image, concert} = updatedUser;
        const payload = {_id, username, image, concert };
       //"updated" token gets generated
        const authToken = jwt.sign(payload, process.env.TOKEN_SECRET, {
          algorithm: "HS256",
          expiresIn: "6h",
        });
        res.json( { updatedUser: payload, authToken })}) // updated token and user are sent to the front end
      .catch(err => res.json(err));
});

And here's how the "create concert" action gets handled on the front end:

 const handleSubmit =  (e) => {
        e.preventDefault();
    
        const storedToken = localStorage.getItem('authToken');
        const addConcert = {
            title, image, description, country,
            city, street, postalCode, houseNumber,
        }

        axios
        .post(`${process.env.REACT_APP_API_URL}/api/concerts`, addConcert, { headers: { Authorization: `Bearer ${storedToken}`}})
        .then( async (response) => {
          const authToken = response.data.authToken;
          const updatedUser = response.data.updatedUser;
          await removeToken() // remove current token
          await storeToken(authToken) // store "up-to-date" token
          await setUser(updatedUser) // update user profile
          navigate("/concerts");
        });
    };

What I came up with works but I thought perhaps there was a different/better way to solve this which wouldn't require generating a new token every time a new concert is created? (it doesn't feel like the best approach so I'm just looking to see what other ways are there that I coudn't think of)

Thanks for reading this, I appreciate your time and suggestions 🙏


Solution

  • I think you're putting too much information in the JWT access token. An access token should contain only information your backend needs to properly authorize requests. Very often this will be just the user's ID, but it can be asserted information about their age or something like a user level (is the user an admin), etc.

    Then, you should have backend endpoints that return the user's data. The concerts created by the user can be kept in a database. (This can be as simple as keeping them in your application's memory, writing them to a local file, or using a dedicated database, like PostgreSQL or MongoDB.) The endpoint should require that the JWT access token is present in each request and should validate it — check whether the signature is fine, whether the token hasn't expired, etc. Then, it can take the information about the user from the token (e.g., the user ID), read the user's data from a database, then return it to your front end. This way, you don't have to update the token whenever the user's data changes, you just need to read the data again after an update. You will only need to update the token once it is expired.