What am I doing wrong with sessions? - javascript

so Ive finally deployed my app and resolved all the CORS issues, but I have a problem with user authentification. I can log-in, but when I refresh a site I get automaticly logged out -> on all browsers beside Mozilla Firefox, there it works somehow.
userContext.js -> Front-end
//XXX Login
const login = () => {
Axios.post(`${apiUrl}/users/login`, {
userName: nameLog,
userPassword: passwordLog,
}).then((response) => {
console.log(response);
if (!response.data.errors) {
setUser(response.data.name);
setUserId(response.data.user_id);
} else {
console.log(response);
const errors = response.data.errors;
console.log(errors);
processErrors(errors);
}
});
};
//Checking if user is logged in on every refresh of a page
useEffect(() => {
Axios.get(`${apiUrl}/users/login`).then((response, err) => {
console.log("GET /login RESPONSE: ", response);
if (response.data.loggedIn === true) {
setUser(response.data.user[0].name);
setUserId(response.data.user[0].user_id);
}
});
}, []);
First call is a POST request, thats when user logs in using form on my site.
And second one is a GET request, that checks if the session returns loggedIn true, this is called on every refresh of a page as it is inside useEffect hook.
Then I update my userState which acts as auth if user is allowed to do some action or not.
userRoutes.js -> Back-end
//Login user
router.post(
"/login",
[
check("userName").exists().notEmpty().withMessage("Username is empty.").isAlpha().isLength({ min: 3, max: 40 }),
check("userPassword").exists().notEmpty().withMessage("Password is empty.").isLength({ min: 3, max: 60 }).escape(),
],
(req, res) => {
const valErr = validationResult(req);
if (!valErr.isEmpty()) {
console.log(valErr);
return res.send(valErr);
}
const name = req.body.userName;
const password = req.body.userPassword;
const sql = "SELECT * FROM users WHERE name = ?";
db.query(sql, name, (err, result) => {
if (err) throw err;
if (!result.length > 0) {
res.send({ errors: [{ msg: "User doesn't exist" }] });
} else {
//compare hashed password from front end with hashed password from DB
bcrypt.compare(password, result[0].password, (error, match) => {
if (error) throw error;
//if passwords match send -> create session and send user data
if (match) {
req.session.user = result;
res.send({ user_id: result[0].user_id, name: result[0].name });
} else {
res.send({ errors: [{ msg: "Wrong username or password" }] });
}
});
}
});
}
);
//Checking if user is logged in and if so, sending user's data to front-end in session
router.get("/login", (req, res) => {
console.log("GET /login SESSION: ", req.session);
if (req.session.user) {
res.send({ loggedIn: true, user: req.session.user });
} else {
res.send({ loggedIn: false });
}
});
Again first one is for the POST request, where I create session and send it in response filled with user's data (name,id) to front-end (then I update my states accordingly).
Second one belongs to the GET request and returns false if user is not logged in or true + user's data. Then once again I update my states.
However this doesnt work and I dont know why. As I mentioned it returns loggedIn: false on every browser besides Mozzilla Firefox.
This is my first time dealing with sessions and cookies so what am I missing here?
By the way the site url is here if that helps: LINK, I left some console.logs() to display responses.
EDIT: adding all middleware
app.js -> main nodejs file
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const cors = require("cors");
const { check, validationResult } = require("express-validator");
const userRoutes = require("./routes/userRoutes.js");
const app = express();
app.use(express.json());
app.use(
cors({
origin: [
"http://localhost:3000",
"https://todo-react-node.netlify.app",
],
methods: ["GET, POST, PATCH, DELETE"],
credentials: true, //allowing cookies
})
);
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
console.log("SESSION 2 : ", req.session);
console.log("Cookies 2 : ", req.cookies);
next();
});
app.use(
session({
key: "userID",
secret: "subscribe",
resave: false,
saveUninitialized: false,
})
);
app.use("/users", userRoutes);

Your useEffect only called once if you are using [] in useEffect function.
Try to take it out(or add the suitable dependencies) and add a console.log() in that function to test out.
Furthermore, read the following article to gain some insight about useEffect.

Okay so finally Ive managed to solve this issue with sessions and cookies. Solution at the end.
What was causing problems?
Cross domain cookies (probably). As Ive mentioned everything worked fine when Ive hosted my app on localhost, but as soon as Ive deployed my app to 2 different hostings (Netlify - front end, Heroku - back end) my cookies werent coming trough.
Ive actually managed to send cookie by
res.cookie("cookie_name", "cookie_value", {sameSite: "none" secure: true})
, however It was standalone cookie and not associated with a session.
Sure why dont you edit your settings in express-session then? Ive tried -> didnt work. My session was never created. I could only send standalone cookie.
And another problem I couldnt delete this cookie in browser after being set, nor I could modify it. Ive tried res.clearCookie("cookie_name") or setting maxAge attribute to -1, nothing worked.
After googling and watching a lot of videos I found out that sending and receiving cookies from different domains is restricted because of security.
SOLUTION -> How did I fix the problem?
Well Ive came upon a VIDEO on YouTube, that showed how to host full-stack application on Heroku. That way your front end and back end are on the same domain and voilĂ  sessions and cookies are working properly. Basically it goes like this:
1) Build your React app
2) In /server (main back end folder) create /public folder and move content of your /client/build directory here
3) In your back end main file (app.js in my case) add app.use(express.static("public")); to serve your front end.
4) Change cors settings to:
app.use(
cors({
origin: "your Heroku app url",
credentials: true, //allowing cookies
}));
5) Change express-session settings
app.use(
session({
name: "name of session",
key: "your key",
secret: "your secret",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
}));
6) Fix your routes for API calls (currently what Im working on)
7) Host it
Create session:
req.session.SESSION_NAME = session_data; Note that SESSION_NAME has to be identical with "name" attribute's value you have declared in step 5
Delete session:
res.clearCookie("SESSION_NAME"); req.session.destroy();
Hope this helps somebody, who encountered this issue.

Related

User session not persisting via React hooks

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.

Express Session Cookie (connect.sid) not being deleted from browser after destroying session

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);
}
});

Facing CORS error even after adding CORS options on React/Node/Passport for Google Authentication

I am building a simple app with React as frontend and Node/Express/MongoDB as backend. I am authenticating user using Passport. Local authentication is working, as well as Google authentication.
I just seem to not able to load the google login page through the app. I am getting CORS error. I have shared the error below.
On React Login page:
const onClick = async () => {
await Axios.get('/auth/google');
};
Proxy Middleware:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(createProxyMiddleware('/auth', { target: 'http://localhost:4000' }));
};
Node Server.js:
app.use('/auth', require('./routes/auth'));
routes/auth file:
const cors = require('cors');
var corsOptions = {
origin: 'http://localhost:3000',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
};
router.get(
'/google',
cors(corsOptions),
passport.authenticate('google', {
scope: ['profile', 'email'],
}),
);
router.get('/google/redirect',cors(corsOptions), passport.authenticate('google'), (req, res) => {
res.send(req.user);
});
passportconfig.js:
passport.use(
new GoogleStrategy(
{
clientID: ClientID,
clientSecret: ClientSecret,
callbackURL: '/auth/google/redirect',
proxy: true,
},
(accessToken, refreshToken, profile, done) => {
// passport callback function
//check if user already exists in our db with the given profile ID
User.findOne({ googleId: profile.id }).then((currentUser) => {
if (currentUser) {
//if we already have a record with the given profile ID
done(null, currentUser);
} else {
//if not, create a new user
new User({
googleId: profile.id,
})
.save()
.then((newUser) => {
done(null, newUser);
});
}
});
},
),
);
Error:
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgoogle%2Fredirect&scope=profile%20email&client_id=<clientID>.apps.googleusercontent.com' (redirected from 'http://localhost:3000/auth/google') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
If I click on the above XMLHttpRequest link, I am able to authenticate and an account is created on my DB with googleID.
I have tried different options suggested throughout internet, but none of them is working for me. I am not sure what is going wrong here.
According to the documentation, try removing the corsOptions entirely and just use the cors() function in your express middle-ware before any router is declared. Like so:
app.use(cors());
Let me know if this works.
// step 1:
// onClick handler function of the button should use window.open instead
// of axios or fetch
const loginHandler = () => window.open("http://[server:port]/auth/google", "_self")
//step 2:
// on the server's redirect route add this successRedirect object with correct url.
// Remember! it's your clients root url!!!
router.get(
'/google/redirect',
passport.authenticate('google',{
successRedirect: "[your CLIENT root url/ example: http://localhost:3000]"
})
)
// step 3:
// create a new server route that will send back the user info when called after the authentication
// is completed. you can use a custom authenticate middleware to make sure that user has indeed
// been authenticated
router.get('/getUser',authenticated, (req, res)=> res.send(req.user))
// here is an example of a custom authenticate express middleware
const authenticated = (req,res,next)=>{
const customError = new Error('you are not logged in');
customError.statusCode = 401;
(!req.user) ? next(customError) : next()
}
// step 4:
// on your client's app.js component make the axios or fetch call to get the user from the
// route that you have just created. This bit could be done many different ways... your call.
const [user, setUser] = useState()
useEffect(() => {
axios.get('http://[server:port]/getUser',{withCredentials : true})
.then(response => response.data && setUser(response.data) )
},[])
Explanation....
step 1 will load your servers auth url on your browser and make the auth request.
step 2 then reload the client url on the browser when the authentication is
complete.
step 3 makes an api endpoint available to collect user info to update the react state
step 4 makes a call to the endpoint, fetches data and updates the users state.

Active Directory Authenticates with Bad Credentials

I am using the npm package activedirectory to connect to my company's domain controller for the purpose of authenticating users on an internal website. The plan is for users to enter their windows domain credentials into the website's login page (written with React). The website will forward the credentials to a backend Express server that looks like this:
const express = require('express');
const bodyParser = require('body-parser');
const ActiveDirectory = require('activedirectory');
const app = express();
app.use(bodyParser.urlencoded({
extended: false,
}));
app.get('/ldap', (request, response) => {
// set http header for json
response.setHeader('ContentType', 'application/json');
// get the username and password from the query
const username = request.query.username + '#internal.mycompany.com';
const password = request.query.password;
console.log(username, password);
// create a new active directory connection
let activeDirectory = new ActiveDirectory({
url: 'LDAP://internal.mycompany.com',
baseDN: 'DC=mycompany,DC=com',
});
// attempt authentication
activeDirectory.authenticate(username, password, (error, authenticated) => {
if(error) {
console.log('ERROR: ' + JSON.stringify(error));
}
else {
console.log('Authentication successful');
}
// respond
response.send(JSON.stringify({
error: error,
authenticated: authenticated,
}));
});
});
app.listen(3001, () => {
console.log('Express server running on port 3001.');
});
The React frontend executes this when the 'Log In' button is clicked:
login() {
console.log('authenticating with username: ', this.state.username, ' and password: ', this.state.password);
fetch(`/ldap/?username=${encodeURIComponent(this.state.username)}&password=${encodeURIComponent(this.state.password)}`).then((response) => {
return response.json();
}).then((response) => {
console.log(response);
this.props.setLoggedIn(response.authenticated);
this.setState({
loginFailed: !response.authenticated,
redirectToHome: response.authenticated,
});
});
}
It calls fetch to ask the Express server to authenticate the credentials, and sets some global loggedIn state accordingly. It also sets local state to display an error message if the login attempt failed or redirect the user on to the main website homepage if the login attempt was successful.
Leaving the username and password blank yields this response:
{
error: {
code: 49,
description: "The supplied credential is invalid",
errno: "LDAP_INVALID_CREDENTIALS",
},
authenticated: false,
}
Typing in a valid username and password yields this response:
{
error: null,
authenticated: true,
}
This is all as expected up to this point.
However, typing in random characters for the username and password yields one of 2 responses. It either authenticates successfully, which shouldn't happen, or it gives this:
{
error: {
lde_dn: null,
lde_message: "80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 52e, v1db1",
},
authenticated: false,
}
QUESTION:
Why would giving the domain controller garbage credentials cause it to return authenticated: true? And why only sometimes? Some information must be cached or remembered somewhere. I tried restarting the Express server and I tried waiting a day for something to expire.
Note: I was planning to encrypt/scramble the passwords so that they are not being sent in plain text, but if there is an even better way to send them securely, please leave a comment about how to improve this. But for now the main question is about the incorrect Active Directory/LDAP authentication.
This information: https://www.rfc-editor.org/rfc/rfc4513#section-6.3.1 essentially tells us that getting authenticated: true may not actually mean anything. The solution I came up with is to try and query the Active Directory to see if the user that just authenticated exists and/or try doing an/some operation(s) as the newly-authenticated user. If the operation(s) fail(s), then the login was not valid.
Additionally, there was an error in the above code. baseDN should have been 'DC=internal,DC=mycompany,DC=com'.

Issue with client-session middleware: req.session_state = undefined after being set

I've hit a bit of a problem getting client-session middleware working in Express. In short the session_state doesn't seem to be accessible when redirecting to new route after being set. For reference I have followed this video tutorial (client-session part starts 36:00 approx.) and double checked my steps but still encountering problems. Middleware is set up as follows:
var sessions = require('client-sessions');
Instantiated with code from the Express website.
app.use(sessions({
cookieName: 'session',
secret: 'iljkhhjfebxjmvjnnshxhgoidhsja',
duration: 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5
}));
I have the sessions middleware placed between bodyParser and routes if that makes any difference.
Here are the sections my routes/index.js pertaining to the issue. The req.session_state seems to get set ok and the correct user details log to the console.
// POST login form
router.post('/login', function(req, res) {
User.findOne( { email: req.body.email }, function(err,user){
if(!user) {
console.log("User not found...");
res.render('login.jade',
{ message: 'Are you sure that is the correct email?'} );
} else {
if(req.body.password === user.password) {
// User gets saved and object logs correctly in the console
req.session_state = user;
console.log("session user...", req.session_state);
res.redirect('/dashboard');
}
}
//res.render('login.jade', { message: 'Invalid password'} );
});
});
However, something is going wrong when the res.redirect('/dashboard'); is run because the session_state is not accessible when it hits that route. Here is the code for the /dashboard route.
router.get('/dashboard', function(req, res) {
// OUTPUT = 'undefined' ???
console.log("dash...", req.session_state);
// 'if' fails and falls through to /login redirect
if(req.session && req.session_state){
console.log("dash route...", req.session_state);
User.findOne( { email: req.session_state.email }, function
(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
} else {
res.locals.user = user;
res.render('dashboard.jade')
}
});
} else {
res.redirect('/login');
}
//res.render('dashboard', { title: 'Your Dashboard' });
});
Basically, the object stored in session_state is not accessible after the /dashboard redirect. I've been trying to debug it for a day or so without any luck. Any help much appreciated. Sorry if I am missing something obvious. Just getting my feet wet with session middleware so maybe I haven't fully grasped Session or I am overlooking something. Thanks in advance!
I've updated my answer with code that should help you set the cookie and an alternative session manager known as a token. In this example I've provided to parts to a middle ware one part which attaches the cookie (this can be expanded on to determine your use case) and the second part which checks the token for expiration or what ever else might be in there (i.e. audience, issuer, etc.)
app.use('/js/',function(req, res, next) {
//check if the request has a token or if the request has an associated username
if(!req.headers.cookie){
console.log('no cookies were found')
var token = jwt.sign({user_token_name:req.body.user_name},app.get('superSecret'), {
expiresIn: 1 *100 *60 // expires in 1 mintue can be what ever you feel is neccessary
});
//make a token and attach it to the body
// req.body.token = token // this is just placing the token as a payload
res.cookie('austin.nodeschool' , token,{ maxAge: 100000, httpOnly: true }) //this sets the cookie to the string austin.nodeschool
}
if(req.body.user_name){
next()
}else{
res.send('request did not have a username').end() // this is here because my middleware also requires a username to be associated with requests to my API, this could easily be an ID or token.
}
},function(req, res, next) {
// console.log(req.headers) this is here to show you the avilable headers to parse through and to have a visual of whats being passed to this function
if(req.headers.cookie){
console.log(req.headers.cookie) //the cookie has the name of the cookie equal to the cookie.
var equals = '=';
var inboundCookie = req.headers.cookie
var cookieInfo = splitCookie(inboundCookie,equals) //splitCookie is a function that takes the inbound cookie and splits it from the name of the cookie and returns an array as seen below.
console.log(cookieInfo)
var decoded = jwt.verify(cookieInfo[1], app.get('superSecret'));
console.log(decoded)
// You could check to see if there is an access_token in the database if there is one
// see if the decoded content still matches. If anything is missing issue a new token
// set the token in the database for later assuming you want to
// You could simply check if it's expired and if so send them to the login if not allow them to proceed through the route.
}
next()
});

Categories

Resources