I know there are a number of questions posted around this and I have read through those, but I cannot seem to solve this issue.
The Set Up
I have my react app running on localhost:3000. My Express Server is running on port 8000 (or 3001 if 8000 is not available. I am using a database to both store the session for the server and as user details. It is a Postgress SQL database running on the default port of 5432 for development.
What works
So, I can fill in the user details in the browser accessing via port localhost:3000. This get gets dispatched via createAsyncThunk which then passes to an API post request. I can see that this is received in the Server, validations are completed, an express session is created, saved in the postgres data base and a response with the user object and a header: set cookie is returned.
The issue
The cookie is not being saved in the browser (I have tried using Chrome and FireFox). I think from reading issues that others had that the problem might be around the configuration of the cookie or cors, but I am not sure.
Here is my code
Backend Express Server 8000
Server Set up
// set up imports
const express = require('express');
require('dotenv').config();
const passport = require('passport');
const initializePassport = require('./controllers/auth.js');
const helmet = require('helmet');
const cors = require('cors');
const session = require('express-session');
const { DB, SS } =require('./config');
//route imports
const { registerRouter, signinRouter, logoutRouter, orderRouter, userRouter } = require('./routes/userRoute.js');
const { productRouter } = require('./routes/productRoute.js');
const { cartRouter } = require('./routes/cartRoute.js');
//server setup
const app = express();
// Used for testing to make sure server / express app is running.
app.get('/', (req, res, next) => {
res.send('<h1>Hello</h1>');
});
app.use(cors({
origin: "localhost:3000",
methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'DELETE', 'HEAD'],
credentials: true,
}));
app.use(helmet());
const pgSession = require('connect-pg-simple')(session);
const { PORT } =require('./config');
const port = PORT || 3001;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const options = {
user: DB.DB_USER,
host: DB.DB_HOST,
database: DB.DB_DATABASE,
password: DB.DB_PASSWORD,
port: DB.DB_PORT,
createDatabaseTable: true,
createTableIfMissing: true
};
console.log(options); //After testing this line can be deleted.
const sessionStore = new pgSession(options);
app.use(session({
name: SS.SS_SESS_NAME,
resave: false,
saveUninitialized: false,
store: sessionStore,
secret: SS.SS_SESS_SECRET,
cookie: {
maxAge: Number(SS.SS_SESS_LIFETIME),
sameSite: 'lax',
secure: false,
domain: "http://localhost:3000",
httpOnly: true,
hostOnly: false,
}
}));
app.use(passport.initialize());
app.use(passport.session());
initializePassport(passport);
//configuration to allow the front end to store cookies created on the server:
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "localhost:3000");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Credentials", true);
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, HEAD, DELETE");
next();
});
//route for users
app.use('/api/users/register', registerRouter);
app.use('/api/users/signin', signinRouter);
app.use('/api/users/logout', logoutRouter);
app.use('/api/users/orders', orderRouter);
app.use('/api/users/details', userRouter);
//route for products
app.use('/api/products', productRouter);
//route for cart
app.use('/api/cart', cartRouter)
//app.listen
app.listen(port, () => {
console.log(`Your server is listening on port: ${port}`);
});
Passport set up
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const pool = require('../db/index');
module.exports = (passport) => {
const query = async (queryString, params) => {
return await pool.query(queryString, params);
};
passport.use(
new LocalStrategy ( async ( username, password, done) => {
try {
const querySchema = { name: 'user', email: `${username}`};
const queryString = `SELECT * FROM user_customer WHERE email = $1`;
const userResult = await query(queryString, [querySchema.email]);
if (userResult.rows.length === 0 ) {
return done(null, false, { message: 'User not found' });
}
const foundUser = userResult.rows[0];
const isMatch = await bcrypt.compare(password, foundUser.password);
if (isMatch) {
return done(null, {
id: foundUser.id,
email: foundUser.email,
firstName: foundUser.first_name,
lastName: foundUser.last_name
});
} else {
return done(null, false, {message: 'Incorrect password '});
}
} catch (err) {
console.error('Error during authentication. ' + err);
return done(err);
}
})
);
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const userResult = await query('SELECT id, email, first_name, AS firstName, last_name AS lastName FROM user_customer WHERE id = $1',
[id]
);
if (userResult.rows.length === 0) {
return done(new Error('User not found'));
} return done(null, userResult.rows[0].id);
} catch(err) {
done(err);
}
});
};
module.exports.isAuth = (req, res, next) => {
if (req.isAuthenticated()) {
next();
} else {
res.status(401).json({ msg: 'You are not authorized to view this resource' });
}
}
Front end React App3000
I have not included the login page code, but can if needed?
Redux Store
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { register, signinUser } from '../apis/apiRequest';
//REGISTER USER DETAILS REMOVED FOR SIZE
export const loginUser = createAsyncThunk('auth/loginUser', async (credentials, { rejectWithValue }) => {
try {
const response = await signinUser(credentials);
console.log(response);
if (!response.user) {
console.warn(`Unable to signin user due to: ${response.message}`);
return rejectWithValue(response.message);
} else {
return response;
}
} catch (error) {
console.error('Error logging in user: ', error);
return rejectWithValue(error.message);
}
});
const initialState = {
isUserLoading: false,
isAuthenticated: false,
error: null,
data: []
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {},
extraReducers: builder => {
builder
//REGISTER USER CASES REMOVED FOR SIZE//
.addCase(loginUser.pending, (state) => {
state.isUserLoading = true;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.isUserLoading = false;
state.isAuthenticated = true;
state.data = action.payload;
})
.addCase(loginUser.rejected, (state, action) => {
state.isUserLoading = false;
state.error = action.payload.message;
})
}
});
export default authSlice.reducer;
export const authData = state => state.auth.data;
export const userAuthLoading = state => state.auth.isUserLoading;
export const userAuthError = state => state.auth.error;
API Request
const API_ROOT = 'http://localhost:8000/api';
//I HAVE REMOVED THE API CALL FOR REGISTER AS I DONT THINK IT IS NEEDED
export const signinUser = async (credentials) => {
const { username, password } = credentials;
const response = await fetch (`${API_ROOT}/users/signin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username,
password
}),
withCredentials: true,
});
const json = await response.json();
return json;
}
Following the request to log in, I am expecting (assuming that the login details are correct) that the server sends a response back to the app which contains the session passport cookie in the header as well as the user object. The Browser stores this this cookie for use later and I can access the user in my redux store.
Here are the screen shots of the request:
Screen shot showing the request object as it goes out and the objects when they return
2
Answers
OK, so the response from Hadi was helpful. I compared what I previously had with what they suggested, and I think the only bits that I changed was the domain: 'localhost' in cookie session settings (previously domain: "http://localhost:3000",) and in the API in the front end client (react app) removed 'withCredentials: true,' and replaced with 'credentials: "include"'.
Just tested it now after making those changes and session cookie is stored in the cookies of the browser.
Correction:
app.use(cors({
origin: "http://localhost:3000",
methods: [‘POST’, ‘PUT’, ‘GET’, ‘OPTIONS’, ‘DELETE’, ‘HEAD’],
credentials: true,
}));
Cookie settings
app.use(session({
name: SS.SS_SESS_NAME,
resave: false,
saveUninitialized: false,
store: sessionStore,
secret: SS.SS_SESS_SECRET,
cookie: {
maxAge: Number(SS.SS_SESS_LIFETIME),
sameSite: ‘lax’,
secure: false,
domain: "localhost",
httpOnly: true,
hostOnly: false,
}
}));
Request Headers API Call
export const signinUser = async (credentials) => {
const { username, password } = credentials;
const response = await fetch(
${API_ROOT}/users/signin
, {method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
credentials: ‘include’,
body: JSON.stringify({
username,
password
}),
});
}
Access-Control Headers
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "http://localhost:3000");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Credentials", true);
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, HEAD, DELETE");
next();
});
Your problem will be fixed with the codes I gave
good luck