OPTIONS request inconsistency (in deployment)? - javascript
Introduction
So, I'm using the MERN stack (with Heroku + Netlify), and I'm having some really strange consistency problems with how the DELETE request is being handled. I've tried countless solutions in the last
three days trying to get this to work and none of them have worked. A lot of these solutions have
come from stack overflow, so if you want to direct me to another post, the chance is that I've already seen it. I've scoured every part of the web and making this post is my last resort.
The Problem
So, when I make a delete request, I'm getting the per-usual OPTIONS request since I'm sending a token in a custom header of the request ('x-auth-token'). The OPTIONS request always resolves with a 204, meaning that everything should be alright. However, afterward, there is no DELETE request like there should be. This, in essence, is my problem. I've checked my Heroku logs, and all I can see is the OPTIONS request, and nothing else.
Inconsistencies?
So this is where I've been very confused. The thing is, that sometimes it DOES work. And other routes I use in my API (like login, and creating a new post) work, even though I'm using the same middleware.
Every time it works, I get the OPTIONS request and then the DELETE request (with a 200 status) like I would expect to happen.
If you want an example of a re-creatable scenario:
I create X number posts after logging in and getting a valid token, then I can see those posts rendering in the posts listing on my home page. I then navigate one of the posts and delete it by clicking and then a confirmation button. I automatically get redirected to the next post in the list. I repeat this till I get to the last post. I delete that post, and since there are no more posts left, I get redirected to the posts listing which is... not empty! The last post I tried deleting is still there.
Keep in mind, that the DELETE requests all get sent in exactly the same way, so I'm pretty sure this isn't a front-end issue, so no need to poke around in the code there. I've logged everything and debugged, and it's 100% consistent with what I would expect.
(The create post doesn't redirect, while the delete post does? I don't see how this would effect anythign as the DELETE request gets sent as per usual... Though maybe a solution lies within this fact.)
Solutions I've tried
Cors
First off, you might already be rushing to your keyboard to tell me that this is a CORS issue. I thought the same thing yesterday, but I'm not so sure now. I've tried messing with all the config settings possible in CORS to get this to work. Since my two websites are on different domains, then CORS verifies the requests. I've already added my front-end website to a whitelist, and all other requests are going through properly, so no problem there. I've tried adding an allowHeaders option in the config, but it didn't do anything more than the default setting. I've also added 'OPTIONS' to the allowed methods in the config, still nothing. I'm also using app.use(cors({config})). I'll include some code later to see some more of this in detail.
Debugging
I've basically tested things out by inserting console.logs everywhere and discovered that neither the middleware, the options route (I tried making an options route with same route url), or the original post route get executed when the OPTIONS request doesn't result in a DELETE request.
Static Server
This is maybe where some of my inexperience shows (this is my first Web project). I saw some solutions telling that a static server is needed. So I tried setting up a static server, but I didn't see any results. So I'm not too sure what this accomplished.
Async and Await
I was just trying things at this point, so I made all my routes async to see if it would do anything. It didn't.
Others
I've also messed around with environment variables and dotenv, and other stuff I can't remember. I think everything here should already be sufficient information understand the situation.
Code
index.js
const express = require('express');
require("dotenv").config({ path: "variables.env" });
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const routes = require("./routes/router");
const cors = require("cors");
const morgan = require('morgan')
const app = express();
const whitelist = [
process.env.ORIGIN
];
app.use(
cors({
origin: function (origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
console.log(origin);
callback(new Error("Not allowed by CORS"));
}
}, //frontend server localhost:3000
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true, // enable set cookie
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(morgan('dev'));
mongoose.connect(process.env.MONGODB_URL, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('connected to db');
});
const userSchema = mongoose.Schema({
name: String,
password: String
});
// Routes
// TODO: make seperate routers/routes
app.use("/", routes);
// Serve static assets if in production
if (process.env.NODE_ENV === 'production') {
// Set static folder
app.use(express.static('client/build'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});
}
// TODO: set up custom port in future
app.listen(process.env.PORT, () => console.log(`Server listening at http://localhost:${process.env.PORT}`));
// Callback functions?
router.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
const User = require('../models/User');
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const auth = require('../middleware/auth');
const adminAuth = require('../middleware/adminAuth');
const cors = require("cors");
require("dotenv").config({ path: "variables.env" });
// import 'moment'
// second onwards are handlers => triggers like the post body then next() to go to the next handler
router.post('/api/add_post', adminAuth, async (req, res, next) => {
try{
newPost = new Post({
title: req.body.title,
body: req.body.body,
author: req.body.author,
created: req.body.created,
});
const savedPost = await newPost.save();
if (!savedUser) throw Error('Something went wrong saving the post');
res.send(savedPost);
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
router.delete('/api/delete_post/:id', adminAuth, async (req, res, next) => {
// timeout?
// console.log(req.body);
try{
const id = req.params.id;
if(!id) throw Error('Invalid ID');
const post = await Post.findById(id);
if (!post) throw Error('Post doesn\'t exist');
const removed = await post.remove();
if(!removed) throw Error('Problem with deleting the post');
res.status(200).json({ success: true });
} catch(e) {
console.log("Error: ", e.message);
res.status(400).json({ msg: e.message, success: false });
}
});
// TODO : UPDATE for async soon
router.post('/api/update_post', adminAuth, async (req, res, next) => {
const id = req.body._id;
test_post_data = {
title: req.body.title,
body: req.body.body,
author: req.body.author,
modified: req.body.modified,
};
console.log(test_post_data, id);
Post.updateOne({ _id: id }, test_post_data, (err) => {
if(err) return next(err);
return res.status(200);
});
});
router.get('/api/get_posts', async (req, res, next) => {
try{
const posts = await Post.find();
if(!posts) throw Error('Error with fetching the posts')
res.send(posts.reverse());
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
router.get('/api/get_chapter/:id', async (req, res, next) => {
try{
const id = req.params.id;
const post = await Post.findOne({_id: id})
if(!post) throw Error('No post was found')
res.send(post);
} catch(e) {
res.status(400).json({ msg: e.message })
}
});
// User routes
// TODO : make in seperate file
router.post('/api/user/register', async (req, res) => {
const { name, email, password } = req.body;
// Simple validation
if (!name || !email || !password) {
return res.status(400).json({ msg: 'Please enter all fields' });
}
try {
const user = await User.findOne({ email });
if (user) throw Error('User already exists');
const salt = await bcrypt.genSalt(10);
if (!salt) throw Error('Something went wrong with bcrypt');
const hash = await bcrypt.hash(password, salt);
if (!hash) throw Error('Something went wrong hashing the password');
const newUser = new User({
name,
email,
password: hash,
admin: false
});
const savedUser = await newUser.save();
if (!savedUser) throw Error('Something went wrong saving the user');
// TODO : check up on expires stuff : 3600 = 1 hr
const token = jwt.sign({ id: savedUser._id, admin: savedUser.admin }, process.env.JWT_SECRET, {
expiresIn: 3600
});
res.status(200).json({
token,
user: {
id: savedUser.id,
name: savedUser.name,
email: savedUser.email,
admin: savedUser.admin
}
});
} catch (e) {
res.status(400).json({ error: e.message });
}
});
router.post('/api/user/login', async (req, res) => {
const { name, password } = req.body;
// Simple validation
if (!name || !password) {
return res.status(400).json({ msg: 'Please enter all fields' });
}
try {
// Check for existing user
const user = await User.findOne({ name });
if (!user) throw Error('User Does not exist');
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw Error('Invalid credentials');
const token = jwt.sign({ id: user._id, admin: user.admin }, process.env.JWT_SECRET, { expiresIn: 3600 });
if (!token) throw Error('Couldnt sign the token');
res.status(200).json({
token,
user: {
id: user._id,
name: user.name,
email: user.email,
admin: user.admin
}
});
} catch (e) {
res.status(400).json({ msg: e.message });
}
});
module.exports = router;
adminAuth.js
const jwt = require('jsonwebtoken')
require("dotenv").config({ path: "variables.env" });
module.exports = (req, res, next) => {
console.log(req.header('x-auth-token'));
const token = req.header('x-auth-token');
// Check for token
if (!token)
return res.status(401).json({ msg: 'No token, authorizaton denied' });
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log('decoded:', decoded);
if(!decoded.admin)
return res.status(401).json({ msg: 'Not an admin, authorization denied' });
// Add user from payload
// console.log('decoded:', decoded);
req.user = decoded;
next();
} catch (e) {
res.status(400).json({ msg: 'Token is not valid' });
}
};
Link for request examples, and Heroku log since Stackoverflow says it's spam:
https://gist.github.com/macklinhrw/b2fec97642882ba406c49cce3e195c39
Edit
I pasted the Chrome request and response headers into the gist at the bottom, but there was no response data to go along with either.
I've debugged a little using this to check the difference and I discovered that with delete action that ends up working, the red (canceled) request has headers, while the non-working is completely empty (filled with 'provisional headers' if that means anything).
I couldn't copy-paste the request headers into the gist for the working red (canceled) one. But, I pasted everything that I thought could possibly be useful from chrome, hopefully it helps.
Also, I didn't see any DELETE requests when I was using the Chrome network tool, and I was seeing them on the other tool. Not sure if it matters, probably just a config option somewhere.
So, I haven't found an exact answer, but I've found a workaround.
As it turns out, it might have something to do with axios, and I've been searching for the wrong things in the last 3 days.
This thread helped me: https://github.com/axios/axios/issues/1428
I've added an e.preventDefault() to the onClick method I use for the delete button.
This fixed the problem, but doesn't redirect (I use href={link}), so I'm going to add a conditional render for react-router to redirect the page. I don't know of a better method so maybe give me some ideas. I'll edit if I have further troubles.
Related
Troubles with nodejs GET endpoint
I have the following endpoint: app.get('/users/:id', async (req, res) => { const _id = req.params.id; try { const user = await User.findById(_id); if(!user) { res.status(404).send(); } res.send(user); } catch (e) { res.status(500).send(e); }}); When I make the request with a valid user ID, the server sends back the user, no problem with that. The problem is when I try to find a user with a ID which doesnt exist in the database. The server should response with a 404 Error but instead it sends back a Error 500 and I dont understand why! Could anyone help me please? Thank you in advance!
One nice way to handle the errors is to create an express error middleware, this allows you to put all of your error handling in one place so that you dont have to write it more than once. With express when you use async routes handlers if a promise rejects the error will automatically be passed to the next error middleware. // First register all of your routes app.get('/user/:id', async (req, res) => { const user = await User.findById(req.params.id); if(!user) return res.status(404).send(); res.send(user); }) // Then register you error middleware app.use((err, req, res, next) => { console.error(err.message) // if mongoose validation error respond with 400 if(err.message.toLowerCase().includes('validation failed')) return res.sendStatus(400) // if moongoose failed because of duplicate key if(err.message.toLowerCase().includes('duplicate key')) return res.sendStatus(409) // if mongoose failed to cast object id if(err.message.toLowerCase().includes('objectid failed')) return res.sendStatus(404) res.sendStatus(500) })
Thank you for your answers. I have solved it adding the following to the user model schema: _id: {type: String} And adding a return before sending the 404 error: app.get('/users/:id', async (req, res) => { const _id = req.params.id; try { const user = await User.findById(_id); if (!user) { return res.status(404).send(); } res.send(user); } catch (error) { res.status(400).send(error); }});
"ErrorTypeError: Cannot read property 'alg' of undefined" JWT
I'm following a tutorial: https://github.com/eXtremeXR/APIAuthenticationWithNode/tree/Part_%2312 And during the sign-up local part, there was an error Unhandled promise rejections are deprecated. But I managed to solved that by using try-catch blocks. But again due to the main error: TypeError: Cannot read property 'alg' of undefined Every request I make returns me an error even though the registration is really successful. The error points me to this block: signToken = user => { return JWT.sign({ iss: 'CodeWorkr', sub: user.id, iat: new Date().getTime(), // current time exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead }, JWT_SECRET); } What's the cause of this and how do I solve it? Here's the full controller code: const JWT = require('jws'); const User = require('../models/user'); const { JWT_SECRET } = require('../configuration'); signToken = user => { return JWT.sign({ iss: 'CodeWorkr', sub: user.id, iat: new Date().getTime(), // current time exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead }, JWT_SECRET); } module.exports = { signUp: async (req, res, next) => { try { const { email, password } = req.body; // Check if there is a user with the same email const foundUser = await User.findOne({ "local.email": email }); if (foundUser) { return res.status(403).json({ error: 'Email is already in use'}); } // Create a new user const newUser = new User({ method: 'local', local: { email: email, password: password } }); await newUser.save(); // Generate the token const token = signToken(newUser); // Respond with token res.status(200).json({ token }); } catch (err) { res.status(500).send({ message: "Error" + err }) } }, signIn: async (req, res, next) => { // Generate token const token = signToken(req.user); res.status(200).json({ token }); }, googleOAuth: async (req, res, next) => { // Generate token const token = signToken(req.user); res.status(200).json({ token }); }, facebookOAuth: async (req, res, next) => { // Generate token const token = signToken(req.user); res.status(200).json({ token }); }, secret: async (req, res, next) => { console.log('I managed to get here!'); res.json({ secret: "resource" }); } }
First off, I got the answer. I'm wondering how is it my tags in this question draw much fewer SO users. It seems to me that you can get answers faster when you're in the mobile development tags. The answer is the project is somehow requiring wrong library? In the sample project, it has: const JWT = require('jws'); But after looking at some more JWT sample projects, I realized it should be: const jwt = require('jsonwebtoken'); solved!
Despite using Cors with Axios I still get this error: No 'Access-Control-Allow-Origin' header is present on the requested resource
I have worked with Cors before in a MEAN stack app. And it perfectly played its role. However, now I am transitioning to the MERN stack, I keep getting this error despite using CORS in the backend: Access to XMLHttpRequest at 'http://localhost:5000/api/users/register' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Register.js:43 Error: Network Error at createError (createError.js:16) at XMLHttpRequest.handleError (xhr.js:83) xhr.js:178 POST http://localhost:5000/api/users/register net::ERR_FAILED So for some reason, CORS isn't doing what it's supposed to do? Here's the main code: Backend/server.js // Use routes app.use("/api/users", users); app.use("/api/profile", profile); app.use("/api/posts", posts); // Use Cors app.use(Cors()); //process.env.Port is for Heroku const port = process.env.Port || 5000; // `` ES6 Template literal is used so that we can put a variable inside the String app.listen(port, () => { console.log(`Server running on port ${port}`); }); Backend/routes/api/users.js // #route POST api/users/register // #desc Register user // #access Public router.post("/register", (req, res) => { // Before we do anything, we will validate the data const { errors, isValid } = validateRegisterInput(req.body); // Check validation if (!isValid) { return res.status(400).json(errors); } // First, we will use mongoose to find if the email exists // Because we don't want someone to register with an email that's already in the db // req.body.email is possible thanks to bodyParser module User.findOne({ email: req.body.email }).then(user => { if (user) { errors.email = "email already exists"; return res.status(400).json({ errors }); } else { const avatar = gravatar.url(req.body.email, { s: "200", //Size r: "pg", //Rating d: "mm" //Default }); // new Modelname({data}) const newUser = new User({ name: req.body.name, email: req.body.email, // This will be the avatar URL //avatar: avatar, // Since they are the same with ES6 we can do just avatar // But it doesn't know what avatar is, for this we will use gravatar avatar, password: req.body.password }); bcrypt.genSalt(10, (err, salt) => { // If there's an error it'll give us an error, if not it'll give us a hash // A hash is what you want to store in the database bcrypt.hash(newUser.password, salt, (error, hash) => { if (error) { throw error; } newUser.password = hash; newUser .save() .then(user => res.json(user)) .catch(err => { console.log(err); }); }); }); } }); }); Frontend/src/components/auth/Register.js axios .post("http://localhost:5000/api/users/register", newUser) .then(res => { console.log(res.data); }) .catch(err => console.log(err));
You have to use cors before using the routes. If you don't do this and since your route handlers don't call next(), cors never has a chance to manipulate your response. // Use Cors app.use(Cors()); // Use routes app.use("/api/users", users); app.use("/api/profile", profile); app.use("/api/posts", posts);
Open it in dev mode: app.use( cors({ origin: (origin, callback) => callback(null, true), credentials: true }) );
Big problem with security (JWT NodeJS), one token for all acces
I have a really big problem with security in my web application. I implemented JWT token when user login to my application (REST API returns token). In my jwt token, I have only userID. Problem is that, when I would like to login on user with ID = 1, I can see and execute rest actions from all other users with the same token. for example: When I looged userId = 1, I doing GET action: /api/users/1 and I have a information about user 1. But I can doing action /api/users/2, 3 etc. All with one token. how to secure it? const jwt = require('jsonwebtoken'); const env = require('../config/env.config.js'); module.exports = (req, res, next) => { try { const token = req.headers.authorization.split(' ')[1]; const decoded = jwt.verify(token, env.SECRET_KEY); req.userData = decoded; next(); } catch (error) { return res.status(401).json({ message: 'Auth failed', }); } };
I think the best solution would be to create middleware that check the id of the sender and attach it to routes, similar to bellow const middleware = (req, res, next) => { const id = req.params.id || req.body.id || req.query.id if (req.userData.id === id) { next() } else { res.status(403).send({message: "forbidden"}) } } router.get("/api/users/:id", middleware, (req, res) => { // do your staff res.send({message: "ok"}) }) router.put("/api/users/:id", middleware, (req, res) => { // do your staff res.send({message: "ok"}) })
Destroying JSON Web Tokens
This is something,that really confuses. me. Let us suppose you have a REST API where you want the user to logout. After login out,the jwt(json web token) should be destroyed,so the user can not have access to the server's resources(ie menu,dishes etc). In my case the user can logout,but he/she can still perform all the requests(get dishes,post and delete),until the token is valid. Here is my code. verify.js var User = require('../models/user'); var jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens var config = require('../config.js'); exports.getToken = function (user) { return jwt.sign(user, config.secretKey, { expiresIn: 3600 }); }; exports.verifyOrdinaryUser = function (req, res, next) { // check header or url parameters or post parameters for token var token = req.body.token || req.query.token || req.headers['x-access-token']; // decode token if (token) { // verifies secret and checks exp jwt.verify(token, config.secretKey, function (err, decoded) { if (err) { var err = new Error('You are not authenticated!'); err.status = 401; return next(err); } else { // if everything is good, save to request for use in other routes req.decoded = decoded; next(); } }); } else { // if there is no token // return an error var err = new Error('No token provided!'); err.status = 403; return next(err); } }; I am invalidating the token after a period of 1 hour. And users.js where I set all the routes with their tasks. ie localhost:3000/users/login,localhost:3000/users/register and localhost:3000/users/logout. So. var express = require('express'); var router = express.Router(); var passport = require('passport'); var User = require('../models/user'); var Verify = require('./verify'); /* GET users listing. */ router.get('/', function(req, res, next) { res.send('respond with a resource'); }); router.post('/register', function(req, res) { User.register(new User({ username : req.body.username }), req.body.password, function(err, user) { if (err) { return res.status(500).json({err: err}); } passport.authenticate('local')(req, res, function () { return res.status(200).json({status: 'Registration Successful!'}); }); }); }); router.post('/login', function(req, res, next) { passport.authenticate('local', function(err, user, info) { if (err) { return next(err); } if (!user) { return res.status(401).json( err: info }); } req.logIn(user, function(err) { if (err) { return res.status(500).json({ err: 'Could not log in user' }); } var token = Verify.getToken(user); res.status(200).json({ status: 'Login successful!', success: true, token: token }); }); })(req,res,next); }); router.get('/logout', function (req, res) { req.logout(); res.status(200).json({ status: 'Bye!' }); }); module.exports = router; It seems that the logout method req.logout,doesn't work:(. Any ideas? Thanks, Theo.
You cannot log out a user that has a valid token if all the data is on the client side. You would need to store some state on the server to distinguish between users that you explicitly logged out and those that you didn't and check this state every time. If all of the data is entirely in the JWT token then you can't do anything to make it invalid (other than changing your secret that would invalidate all of the tokens, not just this one). You actually discovered the main disadvantage of using authentication based entirely on the data that is included in the token itself. Those tokens cannot be invalidated. Once they're out then must be assumed to be active. You could only ask the client to forget it, but the client cannot be trusted to do that. In theory you might have a fast data store like Redis where you keep all of the valid tokens and remove tokens from there to force logout, and check this storage on every request to know who is still logged in and who is not, but if you do that then you might store the session data in Redis in the first place and give only some random keys to that data store to the clients.
JWT is designed to be stateless. This means that all the information needed is contained in the token itself. As the token has already been created, logout will have no effect on the validity of this. This leaves you needing to keep a list of 'invalidated' tokens, which means you have once more introduced state. If you are only concerned about subsequent users on the same machine, you could delete the token on logout, thus preserving the statelessness, but this will not protect against cases where the token has been captured.