Reputation: 2185
I am trying to set up a simple API/SPA using Express as the API (api.mysite.co) and React as the SPA (app.mysite.co) on an AWS ElasticBeanstalk/S3 Deployment. For authentication, I am trying to set up a JWT inside of an HTTP-only secure cookie. But for some reason, the cookie never makes it to the client.
I have changed the domain to mysite.co for security purposes.
Here is my app.js file in express:
const dotenv = require('dotenv');
const express = require('express');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const helmet = require('helmet');
const passport = require('passport');
const cors = require('cors');
const usersRouter = require('./routes/users');
const env = process.env.NODE_ENV || 'development';
const port = process.env.PORT || '3000';
const app = express();
// Setup dev env variables and modules
if (env === 'development') {
dotenv.config();
app.use(logger('dev'));
}
app.use(helmet());
// Setup CORS requests based on environment
if (env === 'development') {
app.use(cors());
} else {
app.use(
cors({
origin: 'https://app.mysite.co',
credentials: true,
}),
);
}
app.use(cookieParser());
// Handle CORS pre-flight reqests
app.options('*', cors());
app.use(passport.initialize());
require('./lib/auth/passport')(passport); // eslint-disable-line global-require
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/users', usersRouter);
// Catch all route that masks 404 for security
app.get('*', (req, res) => {
res.status(401).json({ error: 'Unauthorized access' });
});
app.listen(port);
And my ./routes/users file with the login route. I have confirmed that it is working because the response is send and the user is logged in to the app. But for some reason the cookie is never set in chrome.
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const router = express.Router();
const mongodb = require('../db/mongodb');
const validateRegisterInput = require('../lib/validation/register');
const validateLoginInput = require('../lib/validation/login');
// // POST /users/login - User Login
router.post('/login', (req, res) => {
mongodb.initDb().then((db) => {
const collection = db.collection('users');
const { errors, isValid } = validateLoginInput(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const { email, password } = req.body;
return collection
.find({ email })
.limit(1)
.toArray((connectErr, users) => {
if (connectErr) {
errors.email = 'Could not verify email. Please try again later.';
return res.status(500).json(errors);
}
// Pull user out of array
const user = users[0];
if (!user) {
errors.email = 'Username and Password do not match.';
return res.status(400).json(errors);
}
return bcrypt.compare(password, user.password).then((isMatch) => {
if (isMatch) {
const payload = {
id: user._id, // eslint-disable-line no-underscore-dangle
expire_date: Date.now() + parseInt(process.env.JWT_EXPIRATION_MS, 10),
};
return jwt.sign(JSON.stringify(payload), process.env.PASSPORT_SECRET, (err, token) => {
if (err) {
return res.status(500).json({
password: 'There was an internal error. Please try again later',
});
}
res.cookie('jwt', token, {
domain: 'app.mysite.co',
httpOnly: true,
secure: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
return res.status(200).json({
name: user.name,
});
});
}
errors.password = 'Username and Password do not match.';
return res.status(401).json(errors);
});
});
});
});
module.exports = router;
In the React SPA I am using Axios to make the POST call. In the app.js file I am setting:
axios.defaults.baseURL = 'https://api.mysite.co';
axios.defaults.withCredentials = true;
Then making the call like such:
/**
* @description Logins in new user and sets their token in localStorage
* @param {object} user Input from the login form.
*/
export const loginUser = user => dispatch => axios
.post('/users/login', user)
.then((res) => {
dispatch(setCurrentUser(res.data));
})
.catch((err) => {
dispatch(handleError(err));
});
One thing to note is that when I am sending the request, both the pre-flight OPTIONS and POST calls have this for the request headers:
Provisional headers are shown
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
I have done some research and it seems that when the Provisional headers are shown
warning is shown, it means the API call is not working correctly. But I am getting the follow response from the API on the POST call:
Request URL: https://api.mysite.co/users/login
Request Method: POST
Status Code: 200
Remote Address: 52.201.140.183:443
Referrer Policy: no-referrer-when-downgrade
access-control-allow-credentials: true
access-control-allow-origin: https://app.mysite.co
content-length: 16
content-type: application/json; charset=utf-8
date: Sat, 23 Feb 2019 15:22:33 GMT
etag: W/"10-pyeidcXwb9ByfCNz+iqLTARQNP8"
server: nginx/1.14.1
status: 200
vary: Origin
x-powered-by: Express
Provisional headers are shown
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
Origin: https://app.mysite.co
Referer: https://app.mysite.co/login
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36
Showing that it is a success and then the user is logged in.
Any help would be greatly appreciated since it seems I am way out of my depths here. I am not getting any server or client erros logged and from everything I have read, setting the credentials: true
flag on both Express and Axios should have done the trick.
Thanks!
Upvotes: 2
Views: 5919
Reputation: 2185
So after trying some different things, the issue was with setting the domain in res.cookie
to the subdomain. I changed that code to:
res.cookie('jwt', token, {
domain: 'mysite.co',
secure: true,
httpOnly: true,
maxAge: parseInt(process.env.JWT_EXPIRATION_MS, 10),
});
and now the cookie is being set.
Upvotes: 3