Search code examples
node.jsreactjsexpressjwtexpress-jwt

JsonWebTokenError: jwt must be provided" when using JWT for authentication in Express.js


Description: I am building a web application using Express.js for the backend and React for the frontend. I am implementing user authentication using JSON Web Tokens (JWT). However, I am encountering an error

JsonWebTokenError: jwt must be provided

enter image description here

when trying to verify the token on the server side.

I have the following setup on the server:

I am using jsonwebtoken library for generating and verifying JWT tokens. I have a /login route that handles user login and issues a JWT token upon successful authentication. I also have a /post route that allows authenticated users to create new posts.

Here is the relevant server-side code:

const express = require('express');
const cors = require('cors');
const mongoose = require("mongoose");
const User = require('./models/User');
const Post = require('./models/Post');
const bcrypt = require('bcryptjs');
const app = express();
const jwt = require('jsonwebtoken');
const cookieParser = require('cookie-parser');
const multer = require('multer');
const uploadMiddleware = multer({ storage: multer.memoryStorage() });
const admin = require('firebase-admin');
require('dotenv').config();
const bodyParser = require('body-parser');

const serviceAccount = {
  projectId: process.env.FIREBASE_PROJECT_ID,
  clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
  privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
};

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  storageBucket: process.env.FIREBASE_BUCKET,
});

const bucket = admin.storage().bucket();
const salt = bcrypt.genSaltSync(10);
const secret = process.env.JWT_SECRET;

app.use(cors({
  origin: 'https://personal-website-ea41b.web.app',
  credentials: true,
  optionsSuccessStatus: 200
}));

app.use(express.urlencoded({ extended: false }))
app.use(express.json({limit: '50mb'}));
app.use(cookieParser());

const uri = process.env.MONGO_URL;

mongoose.connect(uri, {
  serverSelectionTimeoutMS: 30000,
});

app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  try {
    const userDoc = await User.create({
      username,
      password: bcrypt.hashSync(password, salt),
    });
    res.json(userDoc);
  } catch (e) {
    console.log(e);
    res.status(400).json(e);
  }
});

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const userDoc = await User.findOne({ username });
  const passOk = bcrypt.compareSync(password, userDoc.password);
  if (passOk) {
    jwt.sign({ username, id: userDoc._id }, secret, {}, (err, token) => {
      console.log('token-secret', secret, token)
      if (err) throw err;
      res.cookie('token', token).json({
        id: userDoc._id,
        username,
        token,
      });
    });
  } else {
    res.status(400).json('wrong credentials');
  }
});

app.get('/profile', (req, res) => {
  const { token } = req.cookies;
  console.log('profile token', token);
  jwt.verify(token, secret, {}, (err, info) => {
    if (err) throw err;
    res.json(info);
  });
});

app.post('/logout', (req, res) => {
  res.cookie('token', '').json('ok');
});

app.post('/post', uploadMiddleware.single('file'), async (req, res) => {
  const { originalname } = req.file;
  const { title, summary, content } = req.body;

  const fileUpload = bucket
    .file(`blog_covers/` + originalname);

  const blobStream = fileUpload.createWriteStream({

    metadata: {
      contentType: req.file.mimetype,
    },
  });

  blobStream.on('error', (err) => {
    console.error(err);
    res.status(500).json('Error uploading file');
  });

  blobStream.on('finish', async () => {
    const [url] = await fileUpload.getSignedUrl({
      action: 'read',
      expires: '03-01-2500',
    });

    const { token } = req.cookies;
    console.log('token',token);
    jwt.verify(token, secret, {}, async (err, info) => {
      if (err) throw err;
      const postDoc = await Post.create({
        title,
        summary,
        content,
        cover: url,
        author: info.id,
      });
      res.json(postDoc);
    });
  });

  blobStream.end(req.file.buffer);
});

app.put('/post', uploadMiddleware.single('file'), async (req, res) => {
  const { id, title, summary, content } = req.body;

  const postDoc = await Post.findById(id);
  const isAuthor = JSON.stringify(postDoc.author) === JSON.stringify(info.id);
  if (!isAuthor) {
    return res.status(400).json('you are not the author');
  }

  const fileUpload = bucket.file(req.file.originalname);

  const blobStream = fileUpload.createWriteStream({
    metadata: {
      contentType: req.file.mimetype,
    },
  });

  blobStream.on('error', (err) => {
    console.error(err);
    res.status(500).json('Error uploading file');
  });

  blobStream.on('finish', async () => {
    const [url] = await fileUpload.getSignedUrl({
      action: 'read',
      expires: '03-01-2500',
    });

    const { token } = req.cookies;
    jwt.verify(token, secret, {}, async (err, decodedToken) => {
      if (err) throw err;
      await postDoc.update({
        title,
        summary,
        content,
        cover: url ? url : postDoc.cover,
      });
      res.json(postDoc);
    });
  });

  blobStream.end(req.file.buffer);
});

app.get('/post', async (req, res) => {
  res.json(
    await Post.find()
      .populate('author', ['username'])
      .sort({ createdAt: -1 })
      .limit(20)
  );
});

app.get('/post/:id', async (req, res) => {
  const { id } = req.params;
  const postDoc = await Post.findById(id).populate('author', ['username']);
  res.json(postDoc);
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

In the client-side code, I am sending the JWT token as a Bearer token in the Authorization header for the requests that require authentication.

Client-side code for login(React):

import { useContext, useState } from "react";
import { Navigate } from "react-router-dom";
import { UserContext } from "../UserContext";
import Cookies from "js-cookie";

export default function LoginPage() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [redirect, setRedirect] = useState(false);
  const { setUserInfo } = useContext(UserContext);
  async function login(ev) {
    ev.preventDefault(); 
    const response = await fetch('https://personal-website-on6a.onrender.com/login', {
      method: 'POST',
      body: JSON.stringify({ username, password }),
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include',
      withCredentials: true,
    });
    if (response.ok) {
      response.json().then(userInfo => {
        setUserInfo(userInfo);
        Cookies.set('token', userInfo.token, { expires: 7 });
        setRedirect(true);
      });
    } else {
      alert('wrong credentials');
    }
  }

  if (redirect) {
    return <Navigate to={'/'} />
  }
  return (
    <form className="login" onSubmit={login}>
      <h1>Login</h1>
      <div className="form-container">
        <input
          type="text"
          placeholder="username"
          value={username}
          onChange={ev => setUsername(ev.target.value)}
        />
        <input
          type="password"
          placeholder="password"
          value={password}
          onChange={ev => setPassword(ev.target.value)}
        />
        <div style={{ width: '400px' }}>
          <button className="btn ac_btn">Login</button>
        </div>
      </div>
    </form>
  );
}

Client-side code for creating post(React):

import 'react-quill/dist/quill.snow.css';
import { useState } from "react";
import { Navigate } from "react-router-dom";
import Editor from "../Editor";
import Cookies from "js-cookie";

export default function CreatePost() {
  const [title, setTitle] = useState('');
  const [summary, setSummary] = useState('');
  const [content, setContent] = useState('');
  const [files, setFiles] = useState('');
  const [redirect, setRedirect] = useState(false);
  const userToken = Cookies.get('token');

  const headers = new Headers({
    'Authorization': `Bearer ${userToken}`,
  });


  async function createNewPost(ev) {
    const data = new FormData();
    data.set('title', title);
    data.set('summary', summary);
    data.set('content', content);
    data.set('file', files[0]);
    ev.preventDefault();
    const response = await fetch('https://personal-website-on6a.onrender.com/post', {
      method: 'POST',
      body: data,
      headers: headers,
      credentials: 'include',
      withCredentials: true,
    });
    if (response.ok) {
      setRedirect(true);
    }
  }

  if (redirect) {
    return <Navigate to={'/indexpage'} />
  }
  return (
    <form onSubmit={createNewPost} style={{ padding: 20, alignItems: 'center', marginTop: "5%" }}>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: "2%" }}>
        <input type="title" style={{ width: '75%', marginBottom: '10px' }}
          placeholder={'Title'}
          value={title}
          onChange={ev => setTitle(ev.target.value)} />
        <input type="summary" style={{ width: '75%', marginBottom: '10px' }}
          placeholder={'Summary'}
          value={summary}
          onChange={ev => setSummary(ev.target.value)} />
        <input type="file"
          style={{ width: '75%', paddingBottom: '50px', marginBottom: '10px' }}
          onChange={ev => setFiles(ev.target.files)} />
        <div style={{ width: '75%', marginBottom: '10px' }}>
          <Editor value={content} onChange={setContent} />
        </div>
        <div style={{ width: '75%' }}>
          <button className="btn ac_btn" style={{ marginTop: '5px' }}>Create post</button>
        </div>
      </div>
    </form>
  );
}

Can someone please help me understand why I am getting this error and how to fix it? Is there something I am missing in the server-side code or in the way I am handling the JWT token on the client-side? Any insights or suggestions would be greatly appreciated. Thank you!


Solution

  • Change your server sided code to parse the token from the header.

    On the client, you are sending the token in the header:

      const headers = new Headers({
        'Authorization': `Bearer ${userToken}`,
      });
    

    But on the server, you are looking for the token in the cookies:

    const { token } = req.cookies;