I am writing a Node JS backend application and Vue JS front, in the API I need session for keep authenticate the users
I use this components on backend server:
express (4.18.2)
express-session (1.17.3)
connect-mongo (3.3.8)
MongoDB 4.4.4
This is the boot code:
// import libs ---
import config from 'config'
import bodyParser from 'body-parser'
import { v4 as uuidv4 } from 'uuid'
import routes from './routes' // custom folder
import express from 'express'
import session from 'express-session'
import MongoStore from 'connect-mongo'
// set costant ---
const app = express()
const secret = config.get('secret')
// body parser ---
app.use(bodyParser.json())
// setup session ---
app.use(session({
genid: function (req) { return uuidv4() }, // random id
secret,
store: MongoStore.create({ // connect to MongoDB fon Session storage
mongoUrl: db.extendedURL,
dbName: db.name,
// autoRemove: 'native',
// autoRemoveInterval: 10, // in minutes
ttl: 7 * 24 * 3600 // in seconds
}),
cookie: { // cookies manage
path: '/',
maxAge: 6000000,
httpOnly: false,
secure: false,
sameSite: false
},
stringify: false,
saveUninitialized: true,
resave: false,
rolling: false,
unset: 'destroy'
}))
// set server port (now run at localhost:5000) ---
app.set('port', 5000)
// set route ---
app.use('/', routes)
In ./route folder there are index.js imports perfectly
Here it is:
// import libs ---
import express from 'express'
const router = express.Router()
// routes ---
router.post('/login', function (req, res) {
const { body, session } = req
try {
session.user = body.user
console.log('user', session.user)
req.session.save(function (err) {
if (err) return res.status(400).json({ message: err })
return res.status(200).json({ message: 'You have successfully authenticated' })
})
} catch (err) {
return res.status(400).json({ message: err })
}
})
router.get('/test', function (req, res) {
const { session } = req
try {
console.log('session', session)
return res.status(200).json(session)
} catch (err) {
return res.status(400).json({ message: err })
}
})
export default router
When I try to call localhost:5000/login (post) I get all just fine, but when I call localhost:5000/test (get) I get a new session on the response and on MongoDB, it also happens whit multiple call on localhost:5000/test
Why express-session generate new session on every call? I really don't know, I spend few days in there and now i don't know how to do
EDIT (12/Jan/2023)
I managet to get it to work using this very simple CORS configuration on NodeJS server boot code:
app.use(cors({
origin: 'http://localhost:8080',
credentials: true
}))
and the header withCredentials: true on every simple http call from front
But this work only from localhost:8080
How can I do for call the Node JS server from other location without specify every IP addres?
Thank you
To allow every origin you can do
app.use(cors({
origin: '*',
credentials: true
}))
or
app.use(cors({
credentials: true,
origin: true
})
Source : Why doesn't adding CORS headers to an OPTIONS route allow browsers to access my API?
Related
I was going through the next-auth documentation but didn't find any mention of connecting to custom configured Redis without the use of Upstash for a persistent session store.
My use case is straightforward. I am using Nginx as a load balancer between multiple nodes for my nextJS application and I would like to persist the session if in case the user logs in and refreshes the page as Nginx switches between nodes.
For e.g My Nginx config
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend;
}
}
upstream backend {
ip_hash;
server <nextjs_app_ip_1>:8000;
server <nextjs_app_ip_2>:8000;
}
As you can see from the example Nginx config, there are multiple upstream server pointers here that require user session persistence.
I am using the credentials provider of next-auth as I have a Django-based auth system already available.
I did see the implementation of the next-auth adapter with Upstash. However, I have my own custom server running with Redis.
I tried connecting to Redis using ioredis which works fine as it is connected. However, I am not sure how can I use Redis here with next-auth to persist session and validate at the same time?
For e.g In express, you have a session store which you can pass your Redis Client with and it should automatically take care of persistence. Is there anything I can do to replicate the same behavior in my case?
For e.g In Express
App.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'secret$%^134',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // if true only transmit cookie over https
httpOnly: false, // if true prevent client side JS from reading the cookie
maxAge: 1000 * 60 * 10 // session max age in miliseconds
}
}))
My Code:
import CredentialsProvider from "next-auth/providers/credentials";
import {UpstashRedisAdapter} from "#next-auth/upstash-redis-adapter";
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL); //points to my custom redis docker container
export const authOptions = {
providers: [CredentialsProvider({
name: 'auth',
credentials: {
email: {
label: 'email',
type: 'text',
placeholder: 'jsmith#example.com'
},
password: {
label: 'Password',
type: 'password'
}
},
async authorize(credentials, req) {
const payload = {
email: credentials.email,
password: credentials.password
};
const res = await fetch(`my-auth-system-url`, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json'
}
});
const user = await res.json();
console.log("user", user);
if (!res.ok) {
throw new Error(user.exception);
}
// If no error and we have user data, return it
if (res.ok && user) {
return user;
}
// Return null if user data could not be retrieved
return null;
}
})],
adapter: UpstashRedisAdapter(redis),
pages: {
signIn: '/login'
},
jwt: {
secret: process.env.SECRET,
encryption: true
},
callbacks: {
jwt: async({token, user}) => {
user && (token.user = user)
return token
},
session: async({session, token}) => {
session.user = token.user
return session
},
async redirect({baseUrl}) {
return `${baseUrl}/`
}
},
session: {
strategy: "jwt",
maxAge: 3000
},
secret: process.env.SECRET,
debug: true
}
export default NextAuth(authOptions)
Thank you so much for the help.
i am new to MERN stack and i don't know why it generates a new session every request. Whenever i request new session/reload the app from the client side which is the react app it always prompt new session id in the backend console.
import Express, {
json
} from 'express'
import * as http from 'http'
import cors from 'cors'
import session from 'express-session'
import {
config
} from 'dotenv'
import MongoStore from 'connect-mongo'
import {
createHmac,
createHash,
scryptSync,
randomBytes,
timingSafeEqual
} from 'crypto'
import connection, {
connectToDb
} from './Services/Connection.js'
connectToDb();
const app = Express()
const port = 5000
const server = http.createServer(app);
app.set('trust proxy', 1) // trust first proxy
app.use(json())
app.use(cors({
credentials: true,
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'DELETE', 'PUT'],
}))
app.use(session({
secret: 'keyboard cat',
resave: true,
saveUninitialized: true,
proxy: true,
cookie: {
secure: false,
expires: 1000 * 60 * 60 * 24,
sameSite: 'none',
httpOnly: true
},
store: MongoStore.create({
mongoUrl: process.env.DB_URI,
ttl: 1000 * 60 * 60 * 24,
autoRemove: "native",
}),
maxAge: 1000 * 60 * 60 * 24,
}))
app.post('/api/login', async (req,res)=>{
const user = req.body;
if(user.type=="guest"){
const guest = await connection.db.collection("user").insertOne({
...user,
})
currentSession ={
...req.session,
user:user
}
console.log(guest)
return;
}
const found = await connection.db.collection("user").findOne({
email:user.email
})
const [salt,password] = found.password.split(":");
const buffPass = Buffer.from(password,"hex",64);
const hashPass = scryptSync(user.password,salt,64);
if(found._id!=undefined && timingSafeEqual(buffPass,hashPass)){
req.session.user = found;
req.session.save((err)=>{
if(err){
return res.send(500);
}
res.send(req.session)
})
}else{
res.send("NO USER FOUND")
}
})
server.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
fetch('/api/login',{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials:'same-origin',
body: JSON.stringify(user),
}).then((e)=>{
console.log("SUCCESS");
navigate("/type=client")
}).catch((r)=>{
console.log("FAILD")
})
Here is my fetch from client side as of now it still generates new session but if i try to fetch directly from the backend it is working fine.
I don't know what ive done wrong maybe there is something with my package.json on the client side. i just the "proxy":"http://localhost:5000" to it. Maybe i need something to make it work?
Before I deploy, the app performed fine on localhost. But since I deployed my frontend (react) to Netlify and backend(node/express + mysql) to Heroku, all requests sent from the frontend started to get blocked by CORS policy, with the error message:
"Access to XMLHttpRequest at 'https://xxx.herokuapp.com/login' from origin 'https://xxx.netlify.app' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://xxx.app/' that is not equal to the supplied origin."
Most importantly, the value of my Access-Control-Allow-Origin header is literally the same as the origin stated.
Originally, I've tried to use a wildcard ("*"), but it seems that due to the withCredential problem, the system just can't allow that kind of vague statement.
I've also seen many people using Netlify.toml to tackle some configuration problems, but seems ineffective for me.
Is it the header's problem? If not, then what is the problem?
I really want to know what I should do to solve this error...
The console window of the app deployed:
Cors Error
My index.js in the server folder:
const express = require('express')
const mysql = require('mysql')
const cors = require('cors')
const session = require('express-session')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const port = 3010
const app = express()
app.use(express.json())
app.use(cors({
origin: ["https://xxx.app/"], // the link of my front-end app on Netlify
methods: ["GET", "POST"],
credentials: true
}))
app.use(cookieParser())
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(
session({
key: "userId",
secret: "subscribe",
resave: false,
saveUninitialized: false,
cookie: {
expires: 60 * 60 * 24
},
})
)
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "https://xxx.netlify.app/"); // the link of my front-end app on Netlify
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PATCH, DELETE, OPTIONS"
);
res.setHeader('content-type', 'application/json');
next();
});
const db = mysql.createPool({
// create an instance of the connection to the mysql database
host: 'xxx.cleardb.net', // specify host name
user: 'xxx', // specify user name
password: 'xxx', // specify password
database: 'heroku_xxx', // specify database name
})
...
app.get('/login', (req, res) => {
if (req.session.user) {
res.send({
isLoggedIn: true,
user: req.session.user
})
} else {
res.send({
isLoggedIn: false
})
}
})
...
app.listen(process.env.PORT || port, () => {
console.log('Successfully Running server at ' + port + '.')
});
My Frontend:
import React, { useEffect, useState } from 'react'
import '../App.css'
import './HeroSection.css'
import { Link } from 'react-router-dom'
import Axios from 'axios'
function HeroSection() {
Axios.defaults.withCredentials = true
let username = "";
const [name, setName] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isLoading, setLoading] = useState(true)
...
useEffect(() => {
Axios.get('https://xxx.herokuapp.com/login').then((response) => {
if (response.data.isLoggedIn) {
username = response.data.user[0].username;
}
setIsLoggedIn(response.data.isLoggedIn)
Axios.post('https://xxx.herokuapp.com/getLang', {
username: username,
}).then((response) => {
console.log(response.data);
})
Axios.post('https://xxx.herokuapp.com/getStatus', {
username: username,
}).then(response => {
setName(response.data[0].firstname + " " + response.data[0].lastname);
setLoading(false);
})
})
}, [])
if (!isLoggedIn || isLoading) {
return (
<div>
...
</div>
)
} else {
return (
<div>
...
</div>
)
}
}
export default HeroSection
By the way, I use ClearDB MySQL on Heroku and MySQL WorkBench for the database, which all works fine.
You could debug by doing something like:
const allowList = ["https://yyy.app/"];
// Your origin prop in cors({})
origin: function (origin, callback) {
// Log and check yourself if the origin actually matches what you've defined in the allowList array
console.log(origin);
if (allowList.indexOf(origin) !== -1 || !origin) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
}
UPDATE: it aparently works on firefox, I was using brave. I guess it's blocking the cookie with the session?? I don't know what to do about that.
I'm building an app with Vue, connecting through Axios to an API made with express.
I'm trying to use express-session to manage login sessions and auth. On my localhost it works great, but when I tried to use it from the site hosted on heroku, it breaks. The middleware that checks whether the session has an usuario property blocks the call.
I'm pretty sure it has to do with https instead of http. I tested it on localhost https with some generated credentials and it broke the same way.
The endpoint for login is quite long, but basically checks if the password you gave it is correct, and if it is, it sets req.session.usuario to an object with some user data. After that, when I check again for the session, usuario is not set.
The CORS middleware:
const cors = require("cors");
const whitelist = ["https://localhost:8080", "https://hosted-client.herokuapp.com"];
const corsOptions = {
credentials: true,
origin: (origin, callback) => {
if (whitelist.includes(origin))
return callback(null, true);
//callback(new Error("CORS error"));
callback(null, true);
},
};
module.exports = cors(corsOptions);
The session middleware:
const Redis = require("ioredis");
const connectRedis = require("connect-redis");
const session = require("express-session");
const RedisStore = connectRedis(session);
const redisClient = new Redis(
process.env.REDIS_PORT,
process.env.REDIS_HOST,
{password: process.env.REDIS_PASSWORD}
);
module.exports = session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SECRET,
saveUninitialized: false,
resave: process.env.STATE_ENV === "production",
proxy: true,
cookie: {
secure: process.env.STATE_ENV === "production",
httpOnly: true,
sameSite: "none",
// maxAge: 1000 * 60 * 30, // 30 minutos
},
});
A simple test auth middleware:
module.exports = function (req, _, next) {
if (!req.session || !req.session.usuario) {
const err = new Error("No se encontró una sesión");
err.statusCode = 401;
next(err);
}
next();
}
The Axios instance on the client:
require("dotenv").config();
import axios from "axios";
export default axios.create({
baseURL: process.env.VUE_APP_API,
headers: {
"Content-type": "application/json",
},
withCredentials: true,
});
I'm not sure if that's enough info, if not let me know.
I'm creating a Next.js web app....
In my app.js file inside the server folder I have the following code:
const express = require("express");
const next = require("next");
const expressValidator = require("express-validator");
const mongoose = require("mongoose");
const passport = require("passport");
const mongoSessionStore = require("connect-mongo");
const session = require("express-session");
/* Loads all variables from .env file to "process.env" */
// npm instsall dotenv
// require("dotenv").config();
require("./models/Listing");
require("./models/User");
const routes = require("./routes");
const dev = process.env.NODE_ENV !== "production";
const port = process.env.PORT || 3000;
const ROOT_URL = dev ? `http://localhost:${port}` : process.env.PRODUCTION_URL;
const app = next({ dev });
const handle = app.getRequestHandler();
const mongooseOptions = {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true
};
mongoose
.connect(
process.env.MONGODB_URI,
mongooseOptions
)
.then(() => console.log("DB connected"));
mongoose.connection.on("error", err => {
console.log(`DB connection error: ${err.message}`);
});
app.prepare().then(() => {
const server = express();
if(!dev) {
/* Helmet helps secure our app by setting various HTTP headers */
server.use(helmet());
/* Compression gives us gzip compression */
server.use(compression());
}
/* Body Parser built-in to Express as of version 4.16 */
server.use(express.json());
/* Express Validator will validate form data sent to the backend */
// server.use(expressValidator());
/* apply routes from the "routes" folder */
server.use("/", routes);
/* give all Next.js's requests to Next.js server */
server.get("/_next/*", (req, res) => {
handle(req, res);
});
server.get("/static/*", (req, res) => {
handle(req, res);
});
const MongoStore = mongoSessionStore(session);
const sessionConfig = {
name: "next-connect.sid",
// secret used for using signed cookies w/ the session
secret: process.env.SESSION_SECRET,
store: new MongoStore({
mongooseConnection: mongoose.connection,
ttl: 14 * 24 * 60 * 60 // save session for 14 days
}),
// forces the session to be saved back to the store
resave: false,
// don't save unmodified sessions
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 14 // expires in 14 days
}
};
if (!dev) {
sessionConfig.cookie.secure = true; // serve secure cookies in production environment
server.set("trust proxy", 1); // trust first proxy
}
/* Apply our session configuration to express-session */
server.use(session(sessionConfig));
/* Add passport middleware to set passport up */
server.use(passport.initialize());
server.use(passport.session());
server.use((req, res, next) => {
/* custom middleware to put our user data (from passport) on the req.user so we can access it as such anywhere in our app */
res.locals.user = req.user || null;
next();
});
server.get("*", (req, res) => {
handle(req, res);
});
server.listen(port, err => {
if (err) throw err;
console.log(`Server listening on ${ROOT_URL}`);
});
})
For some reason I am getting the following error when I run the above code:
UnhandledPromiseRejectionWarning: TypeError: Class constructor
MongoStore cannot be invoked without 'new'
The line that seems to be producing that error seems to be:
const MongoStore = mongoSessionStore(session);
What is causing this and how can I solve this?
So as it turns out the code I was copying was using connect-mongo ^2.0.1 but that connect-mongo I have in the package.json was ^4.4.0
So I needed to change the code suitable from the original version to the new version....and this is what I got at the end:
const sessionConfig = {
name: "next-connect.sid",
// secret used for using signed cookies w/ the session
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
mongooseConnection: mongoose.connection,
ttl: 14 * 24 * 60 * 60 // save session for 14 days
}),
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 14 // expires in 14 days
}
}
if (!dev) {
sessionConfig.cookie.secure = true; // serve secure cookies in production environment
server.set("trust proxy", 1); // trust first proxy
}
/* Apply our session configuration to express-session */
server.use(session(sessionConfig));
and so far I think it is working properly...
I had same issue in my application because I had updated connect-mongo to latest version (4.1). I fixed the issue by uninstalling existing connect-mongo package and re-installed version 3.1.2.
either you can simply install version by typing following command:
npm install connect-mongo#3.1.2
or you copy the following command and paste in your .json file after that type command npm install to change the version:
"connect-mongo": "^3.1.2",
Looks like 'mongoSessionStore' is a Class Constructor. A Class has instructions on how to make a new Object for the session. That happens in a Constructor.
Maybe if you just put the word 'new' before mongoSessionStore it will work?
That way Mongo will make a new Object for the Session that starts.
Maybe it's because you're using connect-mongo#v4, you can try to use npm i connect-mongo#3.2.0 --save to solve this problem.