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
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!
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;