I have created a MERN app that I deployed on Heroku but I can't login although I enter the correct credentials. When I submit the login form, the request to the login api is successful (I get the desired response) but there is no jwt_token cookie stored in the browser and the user context is not updated with the logged in user.
FRONTEND
App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './routes/Home';
import ErrorPage from './routes/ErrorPage';
import LoginPage from './routes/LoginPage';
import RegisterPage from './routes/RegisterPage';
import Tournaments from './routes/Tournaments';
import PlayerStatistics from './routes/PlayerStatistics';
import SettingsPage from './routes/SettingsPage';
import TournamentPlay from './routes/TournamentPlay';
import TournamentCreate from './routes/TournamentCreate';
import MyData from './routes/MyData';
import Header from './components/Header';
import useFindPlayer from './hooks/useFindPlayer';
import { PlayerContext } from './context/PlayerContext';
import ThemeContextProvider from './context/ThemeContextProvider';
import PrivateRoute from './routes/PrivateRoute';
import RedirectLoggedIn from './routes/RedirectLoggedIn';
function App() {
const { player, setPlayer, isLoading } = useFindPlayer();
return (
<div className="App">
<div>
<Router>
<ThemeContextProvider>
<PlayerContext.Provider value={{ player, setPlayer, isLoading }}>
{player && <Header />}
<Routes>
<Route
exact
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
exact
path="/tournaments/:id"
element={
<PrivateRoute>
<TournamentPlay />
</PrivateRoute>
}
/>
<Route
exact
path="/login"
element={
<RedirectLoggedIn>
<LoginPage />
</RedirectLoggedIn>
}
/>
<Route
exact
path="/register"
element={
<RedirectLoggedIn>
<RegisterPage />
</RedirectLoggedIn>
}
/>
<Route
exact
path="/settings"
element={
<PrivateRoute>
<SettingsPage />
</PrivateRoute>
}
/>
<Route element={<ErrorPage />} />
<Route
exact
path="/tournaments"
element={
<PrivateRoute>
<Tournaments />
</PrivateRoute>
}
/>
<Route
exact
path="/players"
element={
<PrivateRoute>
<PlayerStatistics />
</PrivateRoute>
}
/>
<Route
exact
path="/tournaments/new"
element={
<PrivateRoute>
<TournamentCreate />
</PrivateRoute>
}
/>
<Route
exact
path="/mydata"
element={
<PrivateRoute>
<MyData />
</PrivateRoute>
}
/>
</Routes>
</PlayerContext.Provider>
</ThemeContextProvider>
</Router>
</div>
</div>
);
}
export default App;
useAuth.js
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { PlayerContext } from '../context/PlayerContext';
export default function useAuth() {
const navigate = useNavigate();
const { setPlayer } = useContext(PlayerContext);
const [error, setError] = useState(null);
const setPlayerContext = async () => {
return await axios
.get('/api/auth/player')
.then((res) => {
setPlayer(res.data.currentPlayer);
navigate('/');
})
.catch((err) => {
console.error(err);
setError(err.response.data);
});
};
const registerPlayer = async (data) => {
const { username, fullname, password, passwordCheck } = data;
return axios
.post('/api/auth/register', {
username,
fullname,
password,
passwordCheck,
})
.then(async () => {
await setPlayerContext();
navigate('/');
})
.catch((err) => {
console.error(err);
setError(err.response.data);
});
};
//login player
const loginPlayer = async (data) => {
const { username, password } = data;
return axios
.post('/api/auth/login', {
username,
password,
})
.then(async () => {
await setPlayerContext();
})
.catch((err) => {
setError(err.response.data);
});
};
const clearError = () => {
setError(null);
};
return {
registerPlayer,
loginPlayer,
clearError,
error,
};
}
useFindPlayer
import axios from 'axios';
export default function useFindPlayer() {
const [player, setPlayer] = useState(null);
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function findPlayer() {
await axios
.get('/api/auth/player')
.then((res) => {
setPlayer(res.data.currentPlayer);
setLoading(false);
})
.catch((err) => {
console.error(err);
setError(err);
setLoading(false);
});
}
findPlayer();
}, []);
return {
player,
setPlayer,
error,
setError,
isLoading,
};
}
BACKEND
server.js
const express = require('express');
const app = express();
const cors = require('cors');
const PORT = process.env.PORT || 8080;
const mongoose = require('mongoose');
const auth = require('./utils/auth');
const bodyParser = require('body-parser');
const database = findDatabase(process.env.NODE_ENV);
const databaseName = database.split('/')[3].split('?')[0].toUpperCase();
const player = require('./routes/playerRoute');
const game = require('./routes/gameRoute');
const tournament = require('./routes/tournamentRoute');
const userAuth = require('./routes/authRoute');
const cookieParser = require('cookie-parser');
const jwtSecret = process.env.JWT_SECRET;
const path = require('path');
mongoose.connect(database);
function findDatabase(env) {
switch (env) {
case 'production':
return process.env.MONGODB_URI;
case 'development':
return process.env.MONGO_DEV_URI;
case 'demo':
return process.env.MONGO_DEMO_URI;
default:
return process.env.MONGO_DEV_URI;
}
}
const db = mongoose.connection;
const urlEncodedParser = bodyParser.urlencoded({ extended: false });
db.on('error', () => console.error('Error'));
db.once('open', () => {
console.log(`Database ${databaseName} connected...`);
});
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(bodyParser.json(), urlEncodedParser);
app.use(cookieParser(jwtSecret));
// Authentication
app.post('/api/auth/register', userAuth.registerPlayer);
app.post('/api/auth/login', userAuth.loginPlayer);
app.get('/api/auth/logout', userAuth.logoutPlayer);
app.get('/api/auth/player', userAuth.checkPlayer);
// Players
app.post('/api/players', player.add);
app.get('/api/players', player.list);
app.get('/api/players/stats/:id', player.stats);
app.put('/api/players/:id', player.update);
// Tournaments
app.post('/api/tournaments', tournament.add);
app.get('/api/tournaments/complete/:id', tournament.finalize);
app.get('/api/tournaments/:id', tournament.show);
app.get('/api/tournaments', tournament.list);
app.put('/api/tournaments/:id', tournament.update);
app.delete('/api/tournaments/:id', tournament.cancel);
// Games
app.post('/api/games', game.add);
app.get('/api/games', game.list);
app.get('/api/games/:id', game.getOne);
app.delete('/api/tournaments/:tid/game/:gid', game.delete);
//fix login auth and blank page
if (process.env.NODE_ENV === 'production') {
const root = require('path').join(__dirname, 'client', 'build');
app.use(express.static(root));
app.get('*', (req, res) => {
res.sendFile('index.html', { root });
});
} else {
app.get('/', (req, res) => {
res.send('API running');
});
}
app.listen(PORT, () => {
console.log(`Serving on port ${PORT}`);
});
authRoute.js
const catchAsync = require('../utils/catchAsync');
const jwt = require('jsonwebtoken');
const { promisify } = require('util');
const signToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
};
const createPlayerToken = async (player, code, req, res) => {
const token = signToken(player._id);
let d = new Date();
d.setDate(d.getDate() 30);
res.cookie('jwt_token', token, {
expires: d,
httpOnly: true,
secure:
req.secure ||
req.headers['x-forwarded-proto'] === 'https' ||
req.headers['x-forwarded-proto'] === 'http',
sameSite: 'none',
});
player.password = undefined;
res.status(code).json({
status: 'success',
token,
data: {
player,
},
});
};
exports.registerPlayer = async (req, res, next) => {
try {
let { username, password, passwordCheck, fullname } = req.body;
if (!username || !password || !passwordCheck)
return res.status(400).json({ msg: 'Not all fields have been entered.' });
if (password !== passwordCheck) {
return res
.status(400)
.json({ msg: 'Enter the same password twice for verification.' });
}
if (!fullname) {
fullname = username;
}
const newPlayer = await Player.create({
fullname: fullname,
username: username,
password: password,
passwordCheck: passwordCheck,
});
createPlayerToken(newPlayer, 201, req, res);
} catch (err) {
console.log(err);
next(err);
}
};
exports.loginPlayer = catchAsync(async (req, res, next) => {
const { username, password } = req.body;
if (!username || !password) {
return res
.status(400)
.send({ msg: 'Please provide a username and password!' });
}
const player = await Player.findOne({ username }).select(' password');
let correctPassword;
if (player) {
correctPassword = await player.correctPassword(password, player.password);
}
if (!player || !correctPassword) {
return res.status(401).send({ msg: 'Incorrect username or password' });
}
createPlayerToken(player, 200, req, res);
});
exports.checkPlayer = catchAsync(async (req, res, next) => {
let currentPlayer;
if (req.cookies.jwt_token) {
const token = req.cookies.jwt_token;
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
currentPlayer = await Player.findById(decoded.id);
} else {
currentPlayer = null;
}
res.status(200).send({ currentPlayer });
});
//log user out
exports.logoutPlayer = catchAsync(async (req, res) => {
res.cookie('jwt_token', 'loggedout', {
expires: new Date(Date.now() 10 * 1000),
httpOnly: true,
});
res.status(200).send('user is logged out');
});
CodePudding user response:
When using sameSite=None
on a cookie, then you have to use the secure
flag. From samesite docs:
Cookies will be sent in all contexts, i.e. in responses to both first-party and cross-origin requests. If SameSite=None is set, the cookie Secure attribute must also be set (or the cookie will be blocked).
Otherwise, the cookie gets blocked.