1

How to make user redirect after authentication based on user.role ? I'm getting the following error: UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

const jwt = require('jsonwebtoken')
const { COOKIE_NAME, SECRET } = require('../config/config')

module.exports = function() {
    return (req, res, next) => {
        let token = req.cookies[COOKIE_NAME]
        if(token) {
            jwt.verify(token, SECRET, function(err, decoded){
                if (err) {
                    res.clearCookie(COOKIE_NAME)
                } else {
                    if(decoded.user.role === 'admin') {
                        res.redirect('http://localhost:4000')
                    }
                    req.user = decoded;
                }
            })
        }
        next();
    }
}

Login Fetch:

  fetch(`${API}/auth/login`,{
            method: 'POST',
            credentials: 'include',
            withCredentials: true,
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(user)
            })
            .then((response) => {
                if(response.status === 302) {
                    window.location = 'http://localhost:4000'
                }
                else if(response.status === 200) { 
                    onSuccess()
                    setTimeout(() => {
                        window.location = '/'
                    }, 1000)
                } else if (response.status === 401) {
                    onError()
                }
            })
            .catch((error) => {
                console.log(error)
            })
    }

Here is my authService:

const jwt = require('jsonwebtoken')
const User = require('../models/User');
const bcrypt = require('bcrypt')

const { SALT_ROUNDS, SECRET } = require('../config/config');

const register =  async ({name, username, email, password, cart})  => {

    let salt = await bcrypt.genSalt(SALT_ROUNDS);
    let hash = await bcrypt.hash(password, salt);

    const user = new User({
        name,
        username,
        email,
        password: hash,
        cart
    });
    return await user.save()
}

const login = async ({email, password}) => {
    
        let user = await User.findOne({email})
        if (!user) {
            throw {message: 'User not found!'}
        }

        let isMatch = await bcrypt.compare(password, user.password)
        if (!isMatch) {
            throw {message: 'Password does not match!'}
        }

        let token = jwt.sign({user}, SECRET)
        
        return token;
}

And my authController:

const { Router } = require('express');
const authService = require('../services/authService');
const { COOKIE_NAME } = require('../config/config');

const router = Router();

router.post('/login', async (req, res) => {
    const {email, password} = req.body
    try {
        let token = await authService.login({email, password})
        res.cookie(COOKIE_NAME, token)
        res.status(200).json(token)
        } catch (error) {
        res.status(401).json({ error: error })
    }
})

Here is my server if this will help:

app.use((req, res, next) => {
    const allowedOrigins = ['http://localhost:3000', 'http://localhost:4000'];
    const origin = req.headers.origin;
    if (allowedOrigins.includes(origin)) {
         res.setHeader('Access-Control-Allow-Origin', origin);
         res.setHeader('Access-Control-Allow-Credentials', true)
    }
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
    next();
});
Yollo
  • 79
  • 2
  • 9

1 Answers1

1

Since you're using jwt.verify with a callback, it is being executed asynchronously. Due to this, immediately after calling verify but before getting the decoded token, your next() function is called which passes the control to the next middleware (which probably would be synchronous) which then returns the request.

The flow of events would be something like this:

  • if(token) { ... starts
  • jwt.verify(token, ... is called asynchronously. It registers the callback function(err, decoded) { ... but doesn't execute it yet.
  • You exit the if(token) { ... } block and call next().
  • The next middleware in line starts executing and probably returns the request if it is the last middleware in chain. So the client has already been sent the response by this time.
  • jwt.verify(token ... succeeds and calls your registered callback.
  • It sees that there is no error at line if (err) ... so it moves to the else block.
  • It decodes the user role and tries to redirect (which internally would try to insert a header on the response). But this fails because the user was already sent the response (and hence your error message).

So the simple solution to this is to not call next() UNTIL jwt verifies and decodes your token and you know the role. In the code below, I've moved the next() function call a few lines upwards.

const jwt = require('jsonwebtoken')
const { COOKIE_NAME, SECRET } = require('../config/config')

module.exports = function() {
    return (req, res, next) => {
        let token = req.cookies[COOKIE_NAME]
        if(token) {
            jwt.verify(token, SECRET, function(err, decoded){
                if (err) {
                    res.clearCookie(COOKIE_NAME)
                } else {
                    if(decoded.user.role === 'admin') {
                        res.redirect('http://localhost:4000')
                    }
                    req.user = decoded;
                }
                next();
            })
        }
    }
}
thanatonian2311
  • 331
  • 2
  • 11
  • can u check my login fetch request? Now the request stays on pending, and after 3/4 minutes it failed – Yollo Jun 12 '21 at 09:54
  • Are you receiving cookies on the backend? Do the cookies have the token? – thanatonian2311 Jun 12 '21 at 09:58
  • You can check my authService and Controller – Yollo Jun 12 '21 at 10:03
  • In your frontend code, you have added a case for handling 200 and a case for handling 401. But in your backend code, since you are calling `res.redirect()`, your frontend will be receiving a code of 302 ( as can be seen [here](https://expressjs.com/en/4x/api.html#res.redirect) ). Add a case for when `response.status === 302` – thanatonian2311 Jun 12 '21 at 10:08
  • Okay, you can check my fetch request again, if I get it right, but if I'm now I'm getting the following error: `Access to fetch at 'http://localhost:4000/' (redirected from 'http://localhost:5000/auth/login') from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://localhost:5000/' that is not equal to the supplied origin. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors'... ` – Yollo Jun 12 '21 at 10:14
  • I provide part of my server.js I think it will help – Yollo Jun 12 '21 at 10:15
  • And the middleware is the same as yours – Yollo Jun 12 '21 at 10:18
  • You have three port numbers in play: 3000, 4000, 5000. Is that correct? – thanatonian2311 Jun 12 '21 at 10:18
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233688/discussion-between-yollo-and-thanatonian2311). – Yollo Jun 12 '21 at 10:19