I have an ExpressJS API and NextJS UI. I'm trying to generate session tokens. I'm able to generate the token and I see it saved on my MongoDB session storage, but when I try to re-use that session token at on another API endpoint, I get back a 401.
I'm aware of previous questions on the subject and have followed all advice given, and I'm still receiving the same issue. I don't know what else I'm missing.
I'm using containerized node 18.13.0 and the default latest image of mongo.
app.js
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import session from 'express-session';
import helmet from 'helmet';
import MongoDBStore from 'connect-mongodb-session';
import router from './routes/index.js';
app.use(cors({
origin: [
'http://localhost:8080',
'http://127.0.0.1:8080',
'http://127.0.0.1:3000',
'http://localhost:3000',
],
methods: ['POST', 'PUT', 'GET', 'DELETE'],
credentials: true,
}));
app.use(session({
secret: env.SESSION_SECRET,
cookie: {
maxAge: 3600000, // 1hr
httpOnly: true,
rolling: true,
secure: false,
},
store,
resave: false,
saveUninitialized: false,
}));
app.use(router);
./routes/index.js
import Auth from './auth.js';
import crud from './crud.js';
const router = Router();
router.post('/signup', Auth.signup);
router.post('/login', Auth.login);
router.get('/data', Auth.authMiddleware(['admin', 'basic', 'premium', 'standard']), crud.getData);
export default router;
./routes/auth.js
const authMiddleware = (permittedRoles) => async (req, res, next) => {
try {
if (permittedRoles.indexOf(req.session.user.role) > -1) {
next();
} else {
await req.session.destroy();
await res.clearCookie('connect.sid');
res.status(401).json('forbidden');
}
} catch (err) {
await req.session.destroy();
await res.clearCookie('connect.sid');
await res.status(401).json('Session Expired. Log in again.');
}
};
const login = async (req, res) => {
try {
const maybeUser = await UserController.getUser(req.body.email);
if (maybeUser && await bcrypt.compare(req.body.password, maybeUser.password)) {
req.session.user = { email: maybeUser.email, role: maybeUser.role };
req.session.save(); // when I console.log, it works, you see the additional email and role attributes in .user property
res.status(201).json({ success: `Welcome, ${req.body.email}`});
} else {
res.status(400).json('Invalid Email or Password');
}
} catch (err) {
res.status(500).json('Something went wrong on our side. Contact an administrator.');
}
};
const signup = async (req, res) => {
try {
const { email } = req.body;
const password = await bcrypt.hash(req.body.password, 15).then((hash) => hash);
const resp = await UserController.addUser({ email, password });
if (resp.success && !resp.error) {
res.status(201).json({ success: 'User Created.' });
} else {
res.status(400).json('Unable to create account.');
}
} catch (err) {
// res.status(500).json(err);
res.status(500).json('Something went wrong on our side. Contact an administrator.');
}
};
./routes/crud.js
const getData = (req, res) => {
console.log(req.session); // session should include email and role properties but doesn't
....
}
In my nextJS UI, I make the API call like this:
const response = await fetch('localhost:8080/signup', {
method: 'POST',
body: await statesToFormData(),
});
...
const response = await fetch('localhost:8080/login', {
method: 'POST',
body: await statesToFormData(),
});
const response = await fetch('localhost:8080/data', {
method: 'GET',
credentials: 'include',
});
Edit: I solved my problem. req.session(...) takes in a callback, so my login endpoint now looks like this:
const login = async (req, res) => {
...
if (...) {
req.session.user = { email: maybeUser.email, role: maybeUser.role };
req.session.save((err) => {
if (err) {
res.status(500).json(...);
} else {
res.status(201).json(...);
}
});
} else { ... }
...
};