Help with JWT - Using the MERN Stack

Hi, so I made a ToDo app with React, Express, MongoDB and Node and need to allow each user that signs up to have only their todo items show for them.

I have searched as much as possible but can’t come to find a solution to this and please need assistance with this. I know I need to call the JWT token from localstorage and assign it to the task created but how I have no idea as I am at wits end and can’t find a solution to this.

Here is my code in the backend

My todo Schema :point_down:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Create the Scema to be used for the database
const todoSchema = new Schema({
  userId: {type: mongoose.Schema.Types.ObjectId, ref: 'user'},
  task: { type: String, required: true, trim: true }
});

const ToDo = mongoose.model('ToDO', todoSchema)

module.exports = ToDo;

My user Schema :point_down:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

// Create the user Schema
const userSchema = new Schema ({
    name: {
        type: String,
        required: true,
    },
    email: {
        type: String,
        require: true
    },
    password: {
        type: String,
        require: true
    },
    date: {
        type: Date,
        default: Date.now
    }
});

module.exports = User = mongoose.model('users', userSchema);

My todo API :point_down:

const router = require('express').Router();
let ToDo = require('../../models/todo.models');

// Route for the home page, find the todos and render the json file
router.route('/').get((req, res) => {
  ToDo.find()
    .then(todos => res.json(todos))
    .catch(err => res.status(400).json('Error: ' + err));
});
// Method to add a new todo item to the list, declared all the fields
router.route('/add').post((req, res) => {
    const task = req.body.task;
    console.log(req.body)

    const newToDo = new ToDo({
      task
    });
    // Once a new item is added, save it
    newToDo.save()
    .then(() => res.json('todo added!'))
    .catch(err => res.status(400).json('Error: ' + err));
});

// Find a todo by the ID generated by mongoDB
router.route('/:id').get((req, res) => {
  ToDo.findById(req.params.id)
      .then(todo => res.json(todo))
      .catch(err => res.status(400).json('Error: ' + err));
  });

  // Delete a todo by ID from the database
  router.route('/:id').delete((req, res) => {
    ToDo.findByIdAndDelete(req.params.id)
      .then(() => res.json('todo deleted.'))
      .catch(err => res.status(400).json('Error: ' + err));
  });
  
  // Update an item with the specific ID
  router.route('/update/:id').post((req, res) => {
    ToDo.findById(req.params.id)
      .then(todo => {
        todo.task = req.body.task;
  
        todo.save()
          .then(() => res.json('todo updated!'))
          .catch(err => res.status(400).json('Error: ' + err));
      })
      .catch(err => res.status(400).json('Error: ' + err));
  });

module.exports = router;

My user API :point_down:

const express = require("express");
const router = express.Router();
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const keys = require("../../config/keys");
// Load input validation
const validateRegisterInput = require("../../validation/register");
const validateLoginInput = require("../../validation/login");
// Load User model
const User = require("../../models/users.model");

// @route POST api/users/register
// @desc Register user
// @access Public
router.post("/register", (req, res) => {
    // Form validation
    const { errors, isValid } = validateRegisterInput(req.body);
    // Check validation
    if (!isValid) {
        return res.status(400).json(errors);
    }
    User.findOne({ email: req.body.email }).then(user => {
        if (user) {
            return res.status(400).json({ email: "Email already exists" });
        } else {
            const newUser = new User({
                name: req.body.name,
                email: req.body.email,
                password: req.body.password
            });
            // Hash password before saving in database
            bcrypt.genSalt(10, (err, salt) => {
                bcrypt.hash(newUser.password, salt, (err, hash) => {
                    if (err) throw err;
                    newUser.password = hash;
                    newUser
                        .save()
                        .then(user => res.json(user))
                        .catch(err => console.log(err));
                });
            });
        }
    });
});

// @route POST api/users/login
// @desc Login user and return JWT token
// @access Public
router.post("/login", (req, res) => {
    // Form validation
    const { errors, isValid } = validateLoginInput(req.body);
    // Check validation
    if (!isValid) {
        return res.status(400).json(errors);
    }
    const email = req.body.email;
    const password = req.body.password;
    // Find user by email
    User.findOne({ email }).then(user => {
        // Check if user exists
        if (!user) {
            return res.status(404).json({ emailnotfound: "Email not found" });
        }
        // Check password
        bcrypt.compare(password, user.password).then(isMatch => {
            if (isMatch) {
                // User matched
                // Create JWT Payload
                const payload = {
                    id: user.id,
                    name: user.name
                };
                // Sign token
                jwt.sign(
                    payload,
                    keys.secretOrKey,
                    {
                        expiresIn: 31556926 // 1 year in seconds
                    },
                    (err, token) => {
                        res.json({
                            success: true,
                            token: "Bearer " + token
                        });
                    }
                );
            } else {
                return res
                    .status(400)
                    .json({ passwordincorrect: "Password incorrect" });
            }
        });
    });
});

module.exports = router;

My login validation :point_down:

const Validator = require("validator");
const isEmpty = require("is-empty");

module.exports = function validateLoginInput(data) {
    let errors = {};
    // Convert empty fields to an empty string so we can use validator functions
    data.email = !isEmpty(data.email) ? data.email : "";
    data.password = !isEmpty(data.password) ? data.password : "";
    // Email checks
    if (Validator.isEmpty(data.email)) {
        errors.email = "Email field is required";
    } else if (!Validator.isEmail(data.email)) {
        errors.email = "Email is invalid";
    }
    // Password checks
    if (Validator.isEmpty(data.password)) {
        errors.password = "Password field is required";
    }
    return {
        errors,
        isValid: isEmpty(errors)
    };
};

My register validation :point_down:

const Validator = require("validator");
const isEmpty = require("is-empty");

module.exports = function validateRegisterInput(data) {
    let errors = {};
    // Convert empty fields to an empty string so we can use validator functions
    data.name = !isEmpty(data.name) ? data.name : "";
    data.email = !isEmpty(data.email) ? data.email : "";
    data.password = !isEmpty(data.password) ? data.password : "";
    data.password2 = !isEmpty(data.password2) ? data.password2 : "";
    // Name checks
    if (Validator.isEmpty(data.name)) {
        errors.name = "Name field is required";
    }
    // Email checks
    if (Validator.isEmpty(data.email)) {
        errors.email = "Email field is required";
    } else if (!Validator.isEmail(data.email)) {
        errors.email = "Email is invalid";
    }
    // Password checks
    if (Validator.isEmpty(data.password)) {
        errors.password = "Password field is required";
    }
    if (Validator.isEmpty(data.password2)) {
        errors.password2 = "Confirm password field is required";
    }
    if (!Validator.isLength(data.password, { min: 6, max: 30 })) {
        errors.password = "Password must be at least 6 characters";
    }
    if (!Validator.equals(data.password, data.password2)) {
        errors.password2 = "Passwords must match";
    }
    return {
        errors,
        isValid: isEmpty(errors)
    };
};

And lastly my server :point_down:

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const passport = require('passport')
const users = require('./routes/api/users');
const todoRouter = require('./routes/api/todos');
const cors = require('cors');

const app = express();
app.use(cors());

// Body parser middleware
app.use(bodyParser.urlencoded({
  extended: false
}));

app.use(bodyParser.json());

// DB Config
const db = require('./config/keys').mongoURI;

// Connection to mongoDB
mongoose.connect(db, { useNewUrlParser: true, useUnifiedTopology: true }
)
  .then(() => console.log("MongoDB server up and running..."))
  .catch(err => console.log(err));

// Passport middleware
app.use(passport.initialize());

// Passport config
require("./config/passport")(passport);

// Routes
app.use("/api/users", users);
app.use('/api/todos', todoRouter);

const port = process.env.PORT || 5000;

app.listen(port, () => console.log(`Server connected on the following port: ${port}`))

So if you can help out with this and guide me on how to do this I will appreciate it

I link to your actual project would be better (like codesandbox.io or even a GitHub repo), so we do not have to rebuild your app with the code provided.

Hi @RandellDawson, sure here is a GitHub Link

Please excuse the ReadMe.md as I have not added any info here as yet

Hello!

You’re missing a couple of things.

  1. First, you haven’t defined a way to enforce the logged in. You have to define a method that authenticates the user on each request (which is the use of JWT).
  2. You would need to include that token with each request using React.

I would do something like this:

authenticationController.js

function authController() {
  this.isAuthenticated = (req, res, next) => {
    // Require the token from the authorization header:
    const token = req.header('authorization');
    if (!token) {
      // 401 - Unauthorized
      res.sendStatus(401).json('No token provided');
    }
    
    // Verify the authentication token (check if expired, etc.)
    jwt.verify(token.replace('Bearer', '').trim(), 'yourSecretOrKey', (err, decodedToken) => {
    	if (err) {
      	return res.sendStatus(401).json('Invalid token');
      }
      User.findById(decoded.id).exec().then((found) => {
      	if (!found) {
        	throw new Error('No user found with id ' + decoded.id);
        }
      	req.user = res.user = found;
        next();
      }).catch(e => {
      	console.error('Could not retrieve user:', e);
        return res.sendStatus(500).json('An error ocurred on our database');
      });
    })
  }
}

module.exports = authController;

Then, when you need to restrict access to a todo:

const AuthController = require('./authenticationController');
const authController = new AuthController();

router.route('/').get(
  // If the user is authenticated, the next route would execute
  // otherwise it will respond with a 401
  authController.isAuthenticated,
  (req, res) => {
    const authenticatedUser = req.user;
    // Here you're missing something: you need to find only the todos from
    // the authenticated user (assuming the ToDo schema has a user property).
    ToDo.find({user: authenticatedUser.id})
    .then(todos => res.json(todos))
    .catch(err => res.status(400).json('Error: ' + err));
});
1 Like

Hi @skaparate thanks for the response but I dont follow. I created a branch on my gtihub link and I can access the user Bearer token on my get request but not on my post as I now get

VM714:1 POST http://localhost:5001/api/todos/add/ 500 (Internal Server Error)
CreateTodos.js:55 Error: Request failed with status code 500
    at createError (createError.js:16)
    at settle (settle.js:17)
    at XMLHttpRequest.handleLoad (xhr.js:59)

This is when I use axios.post and when I use a fetch method I get Unauthorized.

@hazelbag,

You’re missing the call to passport that will actually authenticate the user:

// routes/api/todos.js

const router = require('express').Router();
let ToDo = require('../../models/todo.models');

// The user here should be authenticated:
router.use(passport.authenticate('jwt', { session: false }));
// Now all the routes you define for this router will require the Bearer token.

// Route for the home page, find the todos and render the json file
router.route('/').get((req, res) => {
  // ToDo.find will retrieve a list of all todos, from all users
  // ToDo.find()
  // You need to filter them per user:
  // req.user is the authenticated user
  ToDo.find({ userId: req.user._id })
    .then(todos => res.json(todos))
    .catch(err => res.status(400).json('Error: ' + err));
});

// Here goes the rest of the file

You need to call passport.authenticate('jwt', { session: false }) for every route that needs authentication, otherwise the router will continue without errors. Here’s another way to use it:

router.route('/public/route').get((req, res, next) => res.json('A public route'));
router.route('/private/route').get(passport.authenticate('jwt', { session: false }), (req, res, next) => res.json('The private route'));

If the bearer token is present, the user will be able to see the /public/route and /private/route endpoints, whereas a user that has not logged in would be able to see only the /public/route and get a 401 if he tries to access the private one.