Problem
I have two express node apps - one acting purely as a REST server and the other is the front end using ejs and sending requests to the back-end using axios. I'm trying to get this working in my dev environment where both apps are running on localhost.
I'm trying to log in at http://localhost:3000/login
which sends the username/password as a POST request to http://localhost:4000/login
and the Set-Cookie
header is being set correctly for the token:
'set-cookie': 'token=[redacted]; Max-Age=28800; Domain=localhost; Path=/; Expires=Wed, 05 Apr 2023 20:42:47 GMT; HttpOnly; SameSite=Strict',
But then when redirected to the http://localhost:3000/transactions
page after success, the cookie is not being sent so the auth fails.
What I've tried
This code is working for cookies using Postman which led me to think it's an issue with Chrome's security changes and/or CORS issue though I'm not getting any CORS error messages in the chrome console.
Other stackoverflow questions seem to confirm this theory but I still haven't been able to successfully send the cookie with subsequent requests to the server (i.e. on the /transactions
call below)
My server side CORS config is this now, I think I've covered all the bases in the above question but I still suspect here's where my problem is:
app.use(cors({
origin: ['http://127.0.0.1:3000','http://localhost:3000'],
methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
exposedHeaders: ['*', 'Authorization', "X-Set-Cookie"]
}));
- I know I need to send the request with credentials, I've tried it both in each request and as a default Just in case it's an axios issue.
- I know browsers have special handling of localhost where it's treated as a secure environment in some scenarios but I'm not clear if this is the case for setting secure cookies - I've switched the values of
secure: true/false
andsameSite: "Strict","None"
to all their combinations but no change. - I tried switching everything (server and client) over to use
127.0.0.1
instead oflocalhost
anyway but there was no change. - I tried installing a local SSL certificate using
mkcert
to make sure I could setsecure: true
andsameSite: "None"
properly but no change. Note I removed this code before posting below as it seems like other people have got this working without needing HTTPS on localhost so I abandoned this and haven't included it in the below code.
Any ideas on what to try next would be greatly appreciated.
Extended code
Server - http://localhost:4000/ index.js
require('dotenv').config({path: './.env'});
const express = require('express');
const logger = require('morgan');
const expressSanitizer = require('express-sanitizer');
const cors = require('cors');
const cookieParser = require('cookie-parser');
let app = express();
app.use(cors({
origin: ['http://127.0.0.1:3000','http://localhost:3000'],
methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
exposedHeaders: ['*', 'Authorization', "X-Set-Cookie"]
}));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(expressSanitizer());
app.use(cookieParser());
app.use(logger('dev'));
app.use(require('./routes/general/authentication.js'));
app.use(require('./handlers/authHandler.js'));
app.use(require('./routes/general/generalRoute.js'));
// Start the server
if(!module.parent){
let port = process.env.PORT != null ? process.env.PORT : 4000;
var server = app.listen(port, 'localhost', function() {
console.log(`Server started on port ${port}...`);
});
}
module.exports = app;
authentication.js
process.env.SECURE_COOKIE = false
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const db = require('../../database/database');
const { check, validationResult } = require('express-validator');
require('dotenv').config({path: './.env'});
const secret = process.env['SECRET'];
const dbURL = process.env['DB_URL'];
const saltRounds = 10;
router.post('/login', [
check('username').exists().escape().isEmail(),
check('password').exists().escape()
], async(req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
res.setHeader('content-type', 'application/json');
if (req.body.username && req.body.password) {
let dbhash = await db.getHashedPassword(req.body.username);
bcrypt.compare(req.body.password, dbhash, async function(err, result) {
if (err) {
res.statusCode = 400;
res.error = err;
return next('Authentication failed! Please check the request');
}
if (result) {
let userData = await db.getUserAuthData(req.body.username);
if (userData.app_access) {
let token = jwt.sign(
{ user_id: userData.id },
secret,
{ expiresIn: '24h' }
);
res.cookie("token", JSON.stringify(token), {
secure: process.env.SECURE_COOKIE === "true",
httpOnly: true,
withCredentials: true,
maxAge: 8 * 60 * 60 * 1000, // 8 hours
sameSite: "Strict",
domain: "localhost"
});
res.statusCode = 200;
res.json({
success: true,
response: 'Authentication successful!'
});
} else {
res.statusCode = 401;
return next('User is not authorised');
}
} else {
res.statusCode = 401;
return next('Incorrect username or password');
}
});
} else {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
} catch (err) {
return next(err);
}
});
module.exports = router;
authHandler.js
var jwt = require('jsonwebtoken');
require('dotenv').config({path: './.env'});
const secret = process.env['SECRET'];
var checkToken = function(req, res, next) {
let token = req.cookies.token;
console.log(req.headers);
if (token) {
// Remove Bearer from string
if (token.startsWith('Bearer ')) {
token = token.slice(7, token.length);
}
// Remove quotes around token
else if (token.startsWith('"')) {
token = token.substring(1, token.length-1);
}
jwt.verify(token, secret, (err, decoded) => {
if (err) {
res.statusCode = 401;
return next('Authentication failed! Credentials are invalid');
} else {
req.decoded = decoded;
next();
}
});
} else {
res.statusCode = 400;
return next('Authentication failed! Please check the request');
}
};
module.exports = checkToken;
Client - http://localhost:3000/
const express = require('express');
const app = express();
const axios = require('axios');
axios.defaults.baseURL = "http://localhost:4000";
axios.defaults.withCredentials = true;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('resources'));
app.set('view engine', 'ejs');
app.set('views', 'views');
app.get('/transactions', async (req, res) => {
axios.get('/budget/transactions', {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
console.log(response.request);
res.render('transactions', { name : 'transactions', title : 'transactions', ...response.data});
})
.catch(err => {
console.log(err);
res.render('transactions', { name : 'transactions', title : 'transactions', ...status });
});
});
app.post('/login', async (req, res) => {
axios.post('/login',
{
username: req.body.email,
password: req.body.password
},
{
withCredentials: true,
headers: {
'content-type': 'application/json',
}
})
.then(response => {
console.log(response.headers);
if (response.data.success === true) {
res.redirect('/transactions');
} else {
res.redirect('/login');
}
});
});
app.listen(3000, () => {
console.log("LISTENING ON PORT 3000");
});