Nodejs Express + Nuxt - Session Cookies - node.js

I am using an Express App for the backend and VueJs with Nuxt (Server Side Rendering). My problem is that the cookies are not getting saved when the session is getting refreshed.
Server:
const express = require('express')
const cookieParser = require('cookie-parser')
const { loadNuxt } = require('nuxt')
const app = express()
app.use(cookieParser())
// Middleware
app.use(async (req, res, next) => {
// ...
if (sessionExpired && refreshTokenIsValid) {
// Generate new session
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
return next()
}
})
...
Login route
router.get('/login', async (req, res, next) => {
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
res.status(200).redirect('/')
})
Client:
async asyncData({ $axios }) {
const data = await $axios.get('/something')
},
methods: {
async someMethod() {
let data = await this.$axios.$get('/something')
}
}

The cookies are not getting saved when sending a request from asyncData().
I solved the problem by using an axios helper.
Solution source: proxy cookies
// plugins/ssr-cookie-proxy.js
import { parse as parseCookie } from 'cookie';
function parseSetCookies(cookies) {
return cookies
.map(cookie => cookie.split(';')[0])
.reduce((obj, cookie) => ({
...obj,
...parseCookie(cookie),
}), {});
}
function serializeCookies(cookies) {
return Object
.entries(cookies)
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
.join('; ');
}
function mergeSetCookies(oldCookies, newCookies) {
const cookies = new Map();
function add(setCookie) {
const cookie = setCookie.split(';')[0];
const name = Object.keys(parseCookie(cookie))[0];
cookies.set(name, cookie);
}
oldCookies.forEach(add);
newCookies.forEach(add);
return [...cookies.values()];
}
export default function ({ $axios, res }) {
$axios.onResponse((response) => {
const setCookies = response.headers['set-cookie'];
if (setCookies) {
// Combine the cookies set on axios with the new cookies and serialize them
const cookie = serializeCookies({
...parseCookie($axios.defaults.headers.common.cookie),
...parseSetCookies(setCookies),
});
$axios.defaults.headers.common.cookie = cookie; // eslint-disable-line no-param-reassign
// If the res already has a Set-Cookie header it should be merged
if (res.getHeader('Set-Cookie')) {
const newCookies = mergeSetCookies(
res.getHeader('Set-Cookie'),
setCookies,
);
res.setHeader('Set-Cookie', newCookies);
} else {
res.setHeader('Set-Cookie', setCookies);
}
}
});
}

Related

axios is not sending the session ID with the request from vue at localhost:5173 to backend at localhost:5000

I am trying to send an axios request from the vue front end to node js backend with sessionID to handle sessions for admins.
this is a login sample to login admins
axios.defaults.withCredentials = true;
const submit = async (data) => {
const { email, password } = data;
const url = "http://localhost:5000/api/v1/users/loginAdmin";
try {
const res = await axios.post(url, {
email,
password,
});
error.value = undefined;
console.log(res.data);
const loginIng = useLogin();
loginIng.logIn(res.data);
router.push("/");
} catch (err) {
console.log(err);
error.value = err.response.data.message;
}
};
after the login is successfully done and that what happened with me, the user is pushed to the root at / and beforeMount the component, there a check to validate the admin credintials in the backend, like this
axios.defaults.withCredentials = true;
setup() {
const login = useLogin();
const router = useRouter();
onBeforeMount(() => {
axios
.post("http://localhost:5000/api/v1/users/validateAdmin")
.then((res) => {
login.logIn(res.data);
})
.catch((err) => {
console.log(err);
router.push("/login");
});
});
return {};
},
In the backend in the app.js, there is a session and cors policy to allow origin from the front end, and session is stored in sqlite3 like the following.
app.use(
session({
store: new KnexSessionStore({
knex: db,
tablename: "sessions",
}),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24,
secure: process.env.NODE_ENV === "production",
},
})
);
in the login admin the session is saving a user with his credintials
like this
const loginAdmin = async (req, res) => {
req.session.user = admin;
}
and this is the ValidateAdmin endpoint sample
const validateAdmin = async (req, res) => {
const userSession = req.session;
const user = userSession.user;
}
the data is being saved to the sqlite3 file in the table, but each time the user visits the endpoint, there is a new id generated for him while i do not need that to happen
What I have already tried:
setting rolling: false, in the session
adding
app.use((req, res, next) => {
req.session.touch();
next();
});
to the app.js
Thanks in advance

Invalid CSRF Token in React but valid in Postman

I have an Express server on which I'm generating a csrf token. My server looks like this
const csrfProtection = csrf({
cookie: {
httpOnly: true,
},
});
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
server.use(
cors({
origin: "http://localhost:3000",
credentials: true,
})
);
server.use(cookieParser());
server.use(csrfProtection);
...
//Other routes
and i'm sending the token like this
export const csrf = (req, res) => {
return res.send({ csrfToken: req.csrfToken() });
};
If I take it from the response and add it to the X-CSRF-Token header in Postman, then I can access all the routes just fine. But when I do it in React I always get the invalid csrf token error
This is how I take the token in React
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf");
axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
And I'm using the withCredentials: true flag on other requests. I can't figure out what I'm missing.
Apparently the problem is that you need to pass the withCredetials flag to the request getting the csrf token too. So this fixed the problem.
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf", { withCredentials: true });
axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
Maybe you should change axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken to axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken

How can I maintain Session in reactJs NodeJs web APP?

I'm developing a register/login website which includes all features to make it work in an efficient and secure way using reactJS, NodeJS and Mysql.
Everything was working fine until I used express-session. In fact, when a user logs in, he will be redirected to a home page (obviously a session will be created) but when the user refreshes the page, It is expected to stay on the home page but the behavior I got is losing the session, thus being redirected to login page.
I looked for a fix and I already tried enabling credentials with Axios in the frontEnd and Cors in the backEnd but the problem is persisting.
This is my code:
server.js
const express = require('express');
const app = express();
const mysql = require('mysql2');
const cors = require('cors');
const validator = require('validator');
const {body, validationResult} = require('express-validator');
const session = require('express-session');
const cookieParser = require('cookie-parser');
app.use(express.json());
app.use(cors({
origin: ['http://localhost:3000'],
methods: ['GET', 'POST'],
credentials: true,
}
));
app.use(express.urlencoded({extended: true}));
app.use(cookieParser());
app.use(session({
name: 'session',
secret: 'crud',
resave: false,
saveUninitialized: false,
cookie: {
expires: 60 * 30,
sameSite: 'strict',
}
}
app.post('/login', (req, res) => {
const mail = validator.escape(req.body.mail);
const pass = validator.escape(req.body.pass);
const sqlSelect = 'SELECT * FROM login WHERE mail = ? AND pass = ?';
db.query(sqlSelect, [mail, pass], (err, result) => {
if (err) {
console.log(err);
}
if (result.length > 0) {
req.session.user = result;
req.session.loggedIn = true;
console.log(req.session.user);
res.send({message: 'success', session: req.session});
}
else {
res.send({message: 'Wrong combination Email/Password !'});
}
})
});
app.get('/login', (req, res) => {
console.log(req.session.user);
if (req.session.user){
res.send({
session: req.session,
message: 'logged'
});
}
else {
res.send({
message: 'Not logged'
});
}
});
app.js (login page)
Axios.defaults.withCredentials = true;
const onSubmit = () => {
Axios.post('http://localhost:9001/login', {
mail,
pass,
}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'success') {
history.push('/home');
}
else {
setMessage(response.data.message);
}
});
};
home.js
export default function Home() {
const [user, setUser] = useState('');
const history = useHistory();
useEffect(() => {
Axios.get('http://localhost:9001/login', {withCredentials: true}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'logged'){
setUser(response.data.session.user[0].mail);
}
else {
history.push('/');
}
})
//eslint-disable-next-line
}, []);
return (
<div>
<p>{user}</p>
</div>
)
}
I hope someone is able to suggest some fix to this. I know I can use localStorage but I want to use the session instead.

How can I solve the Access-Control-Allow-Origin CORS error in my MERN app for MSAL auth?

I'm trying to authenticate through MSAL in my MERN app by clicking a button.
However I get this error :
Access to XMLHttpRequest at
'https://login.microsoftonline.com/common/oauth2/v2.0/a...'
(redirected from 'http://<SERVER_URL>/api/auth/signin') from origin
'http://<CLIENT_URL>' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Here is the code of my NodeJS server :
const express = require("express");
const session = require('express-session');
const authRoutes = require("./routes/auth.routes");
const msal = require('#azure/msal-node');
const cors = require("cors");
require("dotenv").config();
const app = express();
const corsOptions = {
origin : process.env.CLIENT_URL,
credentials: true,
"allowedHeaders": ["sessionId", "Content-Type"],
"exposedHeaders": ["sessionId"],
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false
}
app.use(cors(corsOptions));
// Demo only
app.locals.users = {};
// MSAL Config
const msalConfig = {
auth: {
clientId: process.env.OAUTH_APP_ID,
authority: process.env.OAUTH_AUTHORITY,
clientSecret: process.env.OAUTH_APP_SECRET
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
// Session middleware
app.use(session({
secret: 'your_secret_value_here',
resave: false,
saveUninitialized: false,
unset: 'destroy'
}));
app.use("/api/auth", authRoutes);
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(process.env.PORT, () => {
console.log(`Server is running on port ${process.env.PORT}`);
});
module.exports = app;
Here are my auth.controller methods :
module.exports = {
signIn: async (req, res) => {
const urlParameters = {
scopes: process.env.OAUTH_SCOPES.split(','),
redirectUri: process.env.OAUTH_REDIRECT_URI
};
try {
const authUrl = await req.app.locals.msalClient.getAuthCodeUrl(urlParameters);
res.redirect(authUrl);
} catch (error) {
console.log(`Error: ${error}`);
res.redirect("/");
}
},
callback: async (req, res) => {
const tokenRequest = {
code: req.query.code,
scopes: process.env.OAUTH_SCOPES.split(","),
redirectUri: process.env.OAUTH_REDIRECT_URI
};
try {
const response = await req.app.locals.msalClient.acquireTokenByCode(tokenRequest);
req.session.userId = response.account.homeAccountId;
const user = await graph.getUserDetails(response.accessToken);
req.app.locals.users[req.session.userId] = {
displayName: user.displayName,
email: user.mail || user.userPrincipalName,
timeZone: user.mailboxSettings.timeZone
};
} catch (error) {
console.log(`Error: ${error}`);
}
res.redirect("/");
},
signOut: async (req, res) => {
if (req.session.userId) {
const accounts = await req.app.locals.msalClient.getTokenCache().getAllAccounts();
const userAccount = accounts.find(a => a.homeAccountId === req.session.userId);
if (userAccount) {
req.app.locals.msalClient.getTokenCache().removeAccount(userAccount);
}
}
req.session.destroy(err => res.redirect("/"));
}
};
And here is the React part :
import React from 'react';
import axios from "axios";
const App = () => {
const handleConnect = () => {
axios({
method: "get",
url: `${process.env.SERVER_URL}/api/auth/signin`,
withCredentials: true
})
.then(res => console.log(res.data))
.catch(err => console.log(err));
};
return (
<button onClick={handleConnect}>Connect</button>
);
};
export default App;
In my Azure Active Directory admin center, my redirection URIs are :
"<CLIENT_URL>" as "SPA"
"<SERVER_URL>/api/auth/signin" as "Web"
The Network tab in devtools helps troubleshoot this sort of thing.
You probably need to handle CORS preflight requests, by putting something like this in your express app to handle OPTIONS requests.
app.options('*',cors())
Put this line before app.use() for any routes.
This one bit me in production. Ouch!
Setting Access-Control-Allow-Origin to * is very risky and not recommended. It means that you are allowing any origin to receive a response back from your server.
removing CORS means that Same Origin Policy will be enforced, therefor it won't work.
To solve the issue between your client and server, what you can do is set a proxy in your package.json file of the React app, which will point to your server: "proxy": "YourServerURI".
Regarding the initial question of the error from MSAL, I would suggest to double check that your app is registered correctly and has the permission to access your server.

Why is the req.cookies.session undefined? Firebase + Node + express

I'm trying to assign a generate a session cookie in exchange for the provided ID token. Here are the docs I'm following.
Here is my client side sign-in code:
firebase.auth().signInWithEmailAndPassword(email, password)
.then(function(user) {
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
return sendToken(idToken);
});
})
// .then(() => {
// return firebase.auth().signOut();
// })
.then(() => {
window.location.assign('/member');
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
My send sendToken() posts the idToken to the server:
function sendToken(idToken) {
console.log("Posting " + idToken);
var xhr = new XMLHttpRequest();
var params = `token=${idToken}`;
xhr.open('POST', "/login", true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
return new Promise(function(resolve, reject) {
xhr.onreadystatechange = function() {//Call a function when the state changes.
if (xhr.readyState == 4 && xhr.status == 200) {
resolve();
} else if (xhr.readyState == 4 && xhr.status != 200) {
reject("Invalid http return status");
}
}
return xhr.send(params);
});
}
And at the server, I'm returning a session cookie:
app.post('/login', (req, res) => {
if (req.body.token) {
const idToken = req.body.token.toString();
// Set session expiration to 1 day.
const expiresIn = 60 * 60 * 24 * 1 * 1000;
return firebase.auth().createSessionCookie(idToken, {expiresIn}).then((sessionCookie) => {
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
})
.catch((error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
}
return res.status(400).send("MISSING TOKEN");
});
I've also set up a middleware to verify the session cookie before the server serves the member info:
function authVerification(req, res, next){
const sessionCookie = req.cookies.session || '';
// Verify the session cookie. In this case an additional check is added to
detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
return firebase.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */).then((decodedClaims) => {
console.log("decoded claims: " + decodedClaims);
next();
// serveContentForUser('/profile', req, res, decodedClaims);
}).catch(error => {
// Session cookie is unavailable or invalid. Force user to login.
res.send(error);
});
}
But, when I try to get a member's page after signing in:
app.get("/member", authVerification, (req, res) => {
res.send("member page");
});
I keep getting error from authVerification:
code: "auth/argument-error",
message: "Decoding Firebase session cookie failed. Make sure you passed the entire string JWT which represents a session cookie. See https://firebase.google.com/docs/auth/admin/manage-cookies for details on how to retrieve a session cookie."
Can anyone please point me in the right direction. Thank you in advance!
Solution in the quickstart-nodejs repo.
enter link description here
You have to add cookie-parser and body-parser. Here is how I solved it with Firebase Cloud Functions:
const admin = require("firebase-admin");
const functions = require("firebase-functions");
const next = require("next");
const cors = require("cors");
const express = require("express");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, conf: { distDir: "next" } });
const handle = app.getRequestHandler();
admin.initializeApp({
credential: admin.credential.cert("Service account key"),
databaseURL: "Path to database"
});
const server = express();
server.use(cors({ origin: true }));
server.use(bodyParser.json());
// Support URL-encoded bodies.
server.use(bodyParser.urlencoded({
extended: true
}));
// Support cookie manipulation.
server.use(cookieParser());
// Attach CSRF token on each request.
server.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
function attachCsrfToken(url, cookie, value) {
return function(req, res, next) {
if (req.url === url) {
res.cookie(cookie, value);
}
next();
}
}
server.post("/login", (req, res) => {
if (req.body && req.body.idToken) {
const idToken = `${req.body.idToken}`;
const expiresIn = 60 * 60 * 24 * 5 * 1000;
admin.auth().createSessionCookie(idToken, { expiresIn }).then((sessionCookie) => {
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie("session", sessionCookie, options);
res.end(JSON.stringify({ sessionCookie }));
}, error => {
res.status(401).send(error);
});
} else {
res.status(401).send("Token empty");
}
});
server.post("/profile", (req, res) => {
if (req.cookies && req.cookies.session) {
const sessionCookie = `${req.cookies.session}`;
admin.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */).then((decodedClaims) => {
res.end(JSON.stringify({ decodedClaims }));
}).catch(error => {
res.status(401).send(error);
});
} else {
res.status(401).send("Session empty");
}
});
exports.next = functions.https.onRequest((req, res) => {
if (req.method === "POST") {
if (!req.path) req.url = `/${request.url}`;
return server(req, res);
}
return app.prepare().then(() => handle(req, res));
});
Nothing special on the client side. You can use isomorphic-unfetch, axios or jQuery.

Resources