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.
Related
I wrote a basic authentication code for logging a user in. The code basically generates a JWT token with a given set of params and registers a cookie onto the requesting client.
Now, I want to redirect the user to the last route.
I believe this is done using
res.redirect('back')
Yes, I am using express and React with react-router-dom
Here is the login code:
const login = async (req, res) => {
const { email, password } = req.body
const user = await Users.findOne({
email
})
if (user && user.id) {
// user was found
// check the password
const isValidPassword = bcrypt.compareSync(password, user.password)
if (isValidPassword) {
// generate a JWT token
const authToken = jwt.sign({
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at
}, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
})
// log him in
// const response = {
// id: user.id,
// email: user.email,
// name: user.name,
// created_at: user.created_at
// }
res.cookie(COOKIE_TOKEN, authToken, {
maxAge: +process.env.JWT_EXPIRES_IN,
httpOnly: true
})/*.send(response)*/.redirect('back')
}
else {
res.status(401).send("Incorrect password")
}
}
else {
res.status(404).send({
status: "Failed",
reason: "Incorrect credentials or the user doesn't exist"
})
}
}
Now, the redirect call shows a CORS error on my React app.
Also note, I have used credentials: true for my cors configuration.
Here is the cors part of the code:
app.use(cors({
origin: 'http://localhost:3000',
credentials: true,
optionsSuccessStatus: 200,
}))
The credentials : true is required to establish server-side cookies and that is a must for my application.
Please give me an explanation as to why my redirect is not working and what are the changes required.
P.S. The login route is a POST request
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?
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 am trying to keep a currently user logged in on refresh via server-side authentication. I am using the UseEffect() function to do this in which I verify on refresh.
My issue is that whenever I refresh, my server reads a user session on and off. Meaning that one refresh will read no session, while the other refresh will read a user session, and so on.
I want my app.js to always read code to always read 'auth:true' assuming user is logged in.
Server-side:
index.js
app.use(express.json()); //Parsing Json
app.use(cors({ //Parsing origin of the front-end
origin: ["http://localhost:3000"],
methods: ["GET", "POST"],
credentials: true //Allows cookies to be enabled
}));
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
session({
key: "userID",
secret: "subscribe", //Normally this has to be long and complex for security
resave: false,
saveUninitialized: false,
cookie: { //How long will the cookie live for?
expires: 60 * 60 * 1000, //Expires after one hour
}
}));
app.post('/isUserAuth', (req, res) => { //Where we Authenticate session
const token = req.body.token;
jwt.verify(token, "jwtSecret", (err, decoded) => {
if (err) {
res.send({auth: false, user: "No valid token!"});
}
else if (!req.session.user) {
res.send({auth: false, user: "empty user"});
console.log(req.session.user)
}
else { //Else if user is verified, we send confirmed authentication and user data
res.send({auth: true, user: req.session.user});
console.log(req.session.user)
}
})
});
Client-side:
app.js
const userAuthToken = localStorage.getItem('token');
useEffect(() => { //Stay logged in, if user is logged in, after refresh
Axios.post("http://localhost:3001/isUserAuth", { //End-point for creation request
token: userAuthToken
}).then(response => {
if (!response.data.auth) { //checking for auth status
setAuthStatus(false); //User isnt logged in!
console.log("NOT LOGGED IN!");
console.log(response.data.user);
} else {
setAuthStatus(true); //User is logged into session!
console.log("LOGGED IN!");
console.log(response.data.user);
}
})
}
,[]);
The issue here is that you do not send credentiels from client via the Axios request. Since your server app has credentiels: true, you should do the same thing in the client.
My suggestion is to add {withCredentials: true} to your Hook in the client.
useEffect(() => {
const token = localStorage.getItem('token');
Axios.post("http://localhost:3001/isUserAuth", {
token: token,
},{withCredentials: true} /*<<Insert this*/).then(response => {
if (!response.data.auth) {
setAuthStatus(false);
console.log("NOT LOGGED IN!");
console.log(response.data.user);
} else {
setAuthStatus(true);
console.log("LOGGED IN!");
console.log(response.data.user);
}
})
}
,[]);
This is due to your userToken being undefined on reload.
See this related question on pemanently getting token from local storage.
I have a Vue.js SPA and a Node.js API built with Express.js. I'm using express-session (^1.11.3) to manage sessions and express-sequelize-session (0.4.0) to persist the session on a Postgres DB through Sequelize because I need a session to be able to use passport-azure-ad with oidc strategy.
I was having some issues logging in with Microsoft accounts after some time and came to the conclusion that it is because the session cookie (connect.sid) is never cleared from the browser.
I had some things misconfigured and made some changes but even with all the changes it is still not working.
Session on my Express app is configured in the following way:
import session from 'express-session';
import expressSequelizeSession from 'express-sequelize-session';
const Store = expressSequelizeSession(session.Store);
app.use(session({
cookie: {
path: '/',
httpOnly: true,
secure: env !== 'development', // On environments that have SSL enable this should be set to true.
maxAge: null,
sameSite: false, // Needs to be false otherwise Microsoft auth doesn't work.
},
secret: config.secrets.session,
saveUninitialized: false,
resave: false,
unset: 'destroy',
store: new Store(sqldb.sequelize),
}));
On the FE I'm using Vue.js with Axios and setting withCredentials to true so that the cookie is passed on the HTTP request.
// Base configuration.
import Axios from 'axios';
Axios.defaults.baseURL = config.apiURL;
Axios.defaults.headers.common.Accept = 'application/json';
Vue.$http = Axios;
// When making request.
Vue.$http[action](url, payload, { withCredentials: true }).then(() => // Handle request);
You can see from the image that the cookie is being sent on the logout request.
When logging out I'm hitting this endpoint and destroying the session as is explained on the documentation.
router.post('/logout', (req, res) => {
try {
req.session.destroy(() => {
return responses.responseWithResultAsync(res); // Helper method that logs and returns status code 200.
});
return responses.handleErrorAsync(res); // Helper method that logs and returns status code 500.
} catch (error) {
return responses.handleErrorAsync(res, error); // Helper method that logs and returns status code 500.
}
});
The interesting thing is that the session on the DB is removed so I know that the cookie is being sent properly on the request with the right session ID but it is not removing it on the browser for some reason. After logging out I still have this:
Does anyone know what I'm doing wrong? I find it odd that the session is being removed on the DB successfully but not on the request.
As #RolandStarke mentioned the express-session library doesn't have the built in functionality to remove the cookie from the browser, so I just did it manually in the following way:
router.post('/logout', (req, res) => {
try {
if (req.session && req.session.cookie) {
res.cookie('connect.sid', null, {
expires: new Date('Thu, 01 Jan 1970 00:00:00 UTC'),
httpOnly: true,
});
req.session.destroy((error) => {
if (error) {
return responses.handleErrorAsync(res, error);
}
return responses.responseWithResultAsync(res);
});
}
return responses.responseWithResultAsync(res);
} catch (error) {
return responses.handleErrorAsync(res, error);
}
});