I use express-session to set up session management in the following way:
var express = require('express');
var session = require('express-session');
var app = express();
var mongoose = require('mongoose');
var MongoStore = require('connect-mongo')(session);
mongoose.connect('mongodb://localhost/sessionTest');
app.listen(3000, function () {
});
app.use(session({
secret: 'justsomestuff',
resave: false,
saveUninitialized: false,
cookie: { } ,
store: new MongoStore({mongooseConnection: mongoose.connection})
}));
app.get('/first', function (req, res) {
console.log('first');
req.session.test = 'Tester';
req.session.save();
console.log(req.sessionID);
res.send(req.sessionID);
});
app.get('/second', function (req, res) {
console.log('second');
if (req.session)
console.log('session exists');
console.log(req.sessionID);
console.log(req.session.test);
res.send(req.sessionID);
});
But a new session is established with a new session id every time I call /first or /second with
curl 127.0.0.1:3000/first
curl 127.0.0.1:3000/second
I use
"connect-mongo": "^1.2.1",
"express": "~4.13.4",
"express-session": "^1.14.0",
All sessions are in the mongo database as well.
It seems such a straight forward issue, but I simply cannot figure it out.
You are using curl. and curl don't support cookies implicitly. You have to pass the cookies value explicitly via command. and also read the cookie when the server is trying to set etc..
Why don't you test using browser.?
or If you still want curl then use -b and -c parameters like this.
curl -b cookies.txt -c cookies.txt
-b tells curl to store incoming cookies in cookies.txt and -c tells curl to embed all cookies from cookies.txt into this request.
Moreover Increase cookies life by adding expires field.
app.use(session({
secret: 'justsomestuff',
resave: false,
saveUninitialized: false,
expires: new Date(Date.now() + (60 * 60 * 24 * 7 * 1000)),
cookie: { } ,
store: new MongoStore({mongooseConnection: mongoose.connection})
}));
Related
Upon visiting/refresh, the app checks for a refresh token in the cookie. If there is a valid one, an access token will be given by the Apollo Express Server. This works fine on my desktop but when using Chrome or Safari on the iPhone, the user gets sent to the login page on every refresh.
React App with Apollo Client
useEffect(() => {
fetchUser();
}, []);
const fetchUser = async () => {
const res = await fetch('https://website.com/token', {
method: 'POST',
credentials: 'include',
});
const { accessToken } = await res.json();
if (accessToken === '') {
setIsLoggedIn(false);
}
setAccessToken(accessToken);
setLoading(false);
};
Apollo Client also checks if whether the access token is valid
const authLink = setContext((_, { headers }) => {
const token = getAccessToken();
if (token) {
const { exp } = jwtDecode(token);
if (Date.now() <= exp * 1000) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
}
}
fetch('https://website.com/token', {
method: 'POST',
credentials: 'include',
}).then(async (res) => {
const { accessToken } = await res.json();
setAccessToken(accessToken);
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${accessToken}` : '',
},
};
});
});
const client = new ApolloClient({
link: from([authLink.concat(httpLink)]),
cache: new InMemoryCache(),
connectToDevTools: true,
});
This handles the token link on the Express server
app.use('/token', cookieParser());
app.post('/token', async (req, res) => {
const token = req.cookies.rt;
if (!token) {
return res.send({ ok: false, accessToken: '' });
}
const user = await getUser(token);
if (!user) {
return res.send({ ok: false, accessToken: '' });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ ok: true, accessToken: createAccessToken(user) });
});
And setting of the cookie
export const sendRefreshToken = (res, token) => {
res.cookie('rt', token, {
httpOnly: true,
path: '/token',
sameSite: 'none',
secure: true,
});
};
Same site is 'none' as the front end is on Netlify.
After a day of fiddling and researching, I have found the issue, and one solution when using a custom domain.
The issue is that iOS treats sameSite 'none' as sameSite 'strict'. I thought iOS Chrome would be different than Safari but it appears not.
If you use your front-end, hosted on Netlify, you will naturally have a different domain than your Heroku app back-end. Since I am using a custom domain, and Netlify provides free SSL, half of the work is done.
The only way to set a httpOnly cookie is to set the cookie to secure. The next step would be to set sameSite to 'none' but as mentioned above, this does not work with iOS.
Setting the domain property of the cookie will also not work because the domain property concerns the scope of the cookie and not the cookie origin. If the cookie came from a different domain (Heroku backend), then the frontend (on Netlify) will not be able to use it.
By default, on Heroku, the free dyno will give you a domain like 'your-app.herokuapp.com', which is great because it also includes free SSL. However, for the cookie to work, I added my custom domain that I use with Netlify. To be clear, Netlify already uses my apex custom domain, so I am adding a subdomain to Heroku (api.domain.com). Cookies do work for across the same domain and subdomains with sameSite 'strict'.
The final issue with this is that the custom domain with Heroku will not get SSL automatically, which is why I think it is worth it to upgrade to a $7/month hobby dyno to avoid managing the SSL manually. This I think is the only solution when using a custom domain.
On the other hand, for those who have the same issue and would like a free solution, you can forgo using a custom domain and host your static front-end with the back-end on Heroku.
Hopefully this will save some time for anyone deploying the back-end and front-end separately.
I'm attempting to store a session cookie after a successful firebase-authentication login. All the firebase functions appear to be executing correctly. I'm able to retrieve my idToken after logging. The Post correctly sends this idToken to the server where it is validated. However, I'm getting stuck on the last step which is to store the session cookie in my success function, which doesn't execute.
To troubleshoot this I go to the Chrome Dev Tools and access the network tab and turn on preserve log. Here I can see my post request. When I click on response it says "Failed to load response data". This at least tells me that the post is partially working. The client is at a minimum sending the idToken, but I can't verify that the response is being sent from the server. Additionally, when I navigate to the Application tab of the chrome dev tools and click refresh I don't see my session cookie there.
Here is the client side code:
btnLogin.addEventListener('click', e => {
// Get eamil and pass
// TODO: Check for real email
const email = txtEmail.value;
const pass = txtPassword.value;
// As httpOnly cookies are to be used, do not persist any state client side.
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
// Sign in
firebase.auth().signInWithEmailAndPassword(email,pass).then((userCredential) => {
// Get the user's ID token as it is needed to exchange for a session cookie.
userCredential.user.getIdToken().then((idToken) => {
// Session login endpoint is queried and the session cookie is set.
// TODO: Add CSRF protection);
$.ajax({
type: 'post',
//accepts: 'json',
data: {idToken: idToken},
//dataType: 'json',
contentType: 'application/x-www-form-urlencoded',
success: function(data) {
console.log('MADE IT TO THE SUCCESS FUNCTION');
//document.cookie = data;
firebase.auth().signOut();
$('#loginForm').before(' \
<div class="alert alert-secondary" role="alert"> \
The login was succesful! \
</div> \
');
//setTimeout(window.location.assign('/admin-manage'), 5000);
},
error: function() {
console.log('post error');
$('#loginForm').before(' \
<div class="alert alert-danager" role="alert"> \
The idToken post failed! \
</div> \
');
},
timeout: 5000
});
});
});
});
Here is the server side code:
/* POST FOR SESSION INITIALIZATION */
app.post('/admin-login', (req, res) => {
// Get the ID token passed and the CSRF token.
var idToken = req.body.idToken.toString();
// TODO: CSRF TOKEN
// Set session expiration to 2 days.
const expiresIn = 60 * 60 * 24 * 2 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// We could also choose to enforce that the ID token auth_time is recent.
adminApp.auth().verifyIdToken(idToken).then((decodedIdToken) => {
console.log('ID TOKEN: ' + idToken);
// Only process if the user just signed in the last 5 minutes
if (new Date().getTime() / 1000 - decodedIdToken.auth_time < 5 * 60) {
// Create session cookie and store it serverside
return adminApp.auth().createSessionCookie(idToken, {expiresIn});
}
else {
throw new error('The IDToken has expired');
}
})
// eslint-disable-next-line promise/always-return
.then((sessionCookie) => {
console.log(sessionCookie);
//Set session cookie on client side
const options = {maxAge: expiresIn, httpOnly: false, secure: false};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
})
.catch((error) => {
console.log(error);
res.status(401).send();
});
});
EDIT
Here is an image of the network view from me running chrom dev tools.
CHROME DEV TOOLS APPLICATION VIEW
As you can see the post script gets canceled.
Here is a detailed view of the request headers:
enter image description here
EDIT #2
I've made so more changes to the server-side code and still haven't seen any change in results. Here is the new server-side code:
/* POST FOR SESSION INITIALIZATION */
app.post('/admin-login', (req, res, next) => {
// Get the ID token passed and the CSRF token.
var idToken = req.body.idToken.toString();
// TODO: CSRF TOKEN
// Set session expiration to 2 days.
const expiresIn = 60 * 60 * 24 * 2 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// We could also choose to enforce that the ID token auth_time is recent.
adminApp.auth().verifyIdToken(idToken).then((decodedClaims) => {
console.log('DECODED CLAIMS AUD: ' + decodedClaims.uid );
// Only process if the user just signed in the last 5 minutes
if (new Date().getTime() / 1000 - decodedClaims.auth_time < 5 * 60) {
// Create session cookie and store it serverside
// eslint-disable-next-line promise/no-nesting
adminApp.auth().createSessionCookie(idToken, {expiresIn}).then((sessionCookie) => {
console.log(sessionCookie);
//Set session cookie on client side
const options = {
maxAge: expiresIn,
httpOnly: true,
secure: false
};
res.set('Content-Type', 'application/json');
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
});
}
else {
throw new error('The IDToken has expired');
}
}).catch((error) => {
console.log(error);
res.status(401).send('UNAUTHROIZED REQUEST!');
});
});
I also took a wireshark capture on my loopback adapter so that I could capture the localhost traffic. I can see the HTTP response with the set-cookie tag, but not sure what the problem is. See the image below:
HTTP Post Response Wireshark Capture
EDIT #3
I've finally made a little progress. I noticed the URI in my wireshark capture wasn't "samesite" so I added the "sameSite: 'None'" option to my set-cookie options.
So my new options are:
const options = {
maxAge: expiresIn,
httpOnly: true,
secure: false,
sameSite: 'None'
};
This fixed the issue on my localhost development environment. However, I changed the secure option to true then did a firebase deploy. After the deployment I logged into the production environment and found that the cookie would not generate. So the only question left is why it won't work in the production environment.
EDIT #4
Alright, I'm losing my mind. I changed the options back to how I had them in EDIT #3 so that I could continue working the app, and the session cookie is no longer being set again. I'm at a complete loss as to why it won't work now. I even went into chrome to clear all browsing history in hopes this would resolve the issue, but no luck. If anyone can shed light on what is happening here I would greatly appreciate it.
I'm using Electron (v1.2.7) and I need session cookies to persist between app restarts (for auth).
Is it possible? I know in Chrome when you set "Continue where you left off", the session is kept but not sure if this works the same in Electron.
As a workaround, I tried storing the session cookies also as non session cookies but it failed with a generic error.
Clarification: I'm not setting the session cookies they are set by other webpages during authentication.
The default session is persistent, but if you set cookies using session.defaultSession.cookies.set() you must set the expiration date in order for the cookie to be persisted.
You can persist cookies setting the session and a expirationDate
This example was made on Angularjs
var session = require('electron').remote.session;
var ses = session.fromPartition('persist:name');
this.put = function (data, name) {
var expiration = new Date();
var hour = expiration.getHours();
hour = hour + 6;
expiration.setHours(hour);
ses.cookies.set({
url: BaseURL,
name: name,
value: data,
session: true,
expirationDate: expiration.getTime()
}, function (error) {
/*console.log(error);*/
});
};
PD: A problem that i had was if i didn't persist them, after a fews reloads my cookies get lost. So in this example i'd persist them 6 hours
If you are loading an external webpage, you should be using Electrons <webview> tag to disable nodeintegration and for other security reasons. Using a webview will give you easy access to the Partition attribute which can be set to persist (ex: persist:mypage). You can also set the partition attribute on an Electron window if needed.
I was loading an external webpage and the configuration below worked for me. By default the webpage is configured to use "session cookie" and thats why I change it to "persistent cookie" with expiration date of 2 weeks:
// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
const path = require('path')
const util = require('util')
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 700,
height: 500,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
partition: 'persist:infragistics'
},
icon: __dirname + '/assets/favicon.ico',
show:false
})
let cookies = mainWindow.webContents.session.cookies;
cookies.on('changed', function(event, cookie, cause, removed) {
if (cookie.session && !removed) {
let url = util.format('%s://%s%s', (!cookie.httpOnly && cookie.secure) ? 'https' : 'http', cookie.domain, cookie.path);
console.log('url', url);
cookies.set({
url: url,
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
expirationDate: new Date().setDate(new Date().getDate() + 14)
}, function(err) {
if (err) {
log.error('Error trying to persist cookie', err, cookie);
}
});
}
});
Note: Its important to ensure that you've set the "partition" webPreferences property as well.
A String that sets the session used by the page. If partition starts with persist:, the page will use a persistent session available to all pages in the app with the same partition. if there is no persist: prefix, the page will use an in-memory session
Origin source.
I am using Redis to store my session data for my ExpressJS application and in the past have run into some issues where the persistent cookie is keeping my user logged in causing issues with development. I have tried to clear my session data with the redis-cli, but despite running DEL KEYS * and being given the (integer) 0 response, I still see the sessions appear when I run KEYS *. Can anyone help me with removing that data?
Example:
127.0.0.1:6379> KEYS *
1) "sess:O7pchKqe-n7NUhP3lBANaf7LMjJG0U0a"
2) "sess:tSyQCCISPBpH88zT3MJjHw2tidttMdRs"
127.0.0.1:6379> DEL KEYS *
(integer) 0
127.0.0.1:6379> KEYS *
1) "sess:O7pchKqe-n7NUhP3lBANaf7LMjJG0U0a"
2) "sess:tSyQCCISPBpH88zT3MJjHw2tidttMdRs"
ExpressJS code storing the session data:
var express = require('express');
var app = express();
var passport = require('passport');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var RedisStore = require('connect-redis')(session);
var path = require('path');
//Port Setting
var port = process.env.PORT || 3000;
//Set Morgan as Logger
app.use(morgan('dev'));
//Extract POST Data
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
//Session Cookie
app.use(cookieParser());
app.use(session({
store: new RedisStore({
host: '127.0.0.1',
port: 6379
}),
secret: 'super-secret',
resave: true,
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: false //turn to true on production once https is in place
}
}));
(transferring from comment)
I think you want flushdb to delete all keys. del doesn't have a way to specify a wildcard, so del keys * will try to delete two keys: one called "keys" and one called "*". The response 0 means that 0 keys were deleted.
I have a question about express session store memory with setting the reapInterval value. I have an example code, which will output the values of a memorystore every 5 seconds. If i now set a reapinterval of 5000, it should clean up expired session every 5 seconds right? So my example looks like this:
/**
* Module dependencies.
*/
var express = require('express');
var app = module.exports = express.createServer();
var MemStore = require('express/node_modules/connect/lib/middleware/session/memory');
var store = new MemStore({reapInterval: 5000});
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({secret: 'your secret here', store: store, cookie: {maxAge: 30000}}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
setInterval(function(){
console.log(new Date());
console.log(store);
}, 5000);
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', function(req, res){
res.render('index', {
title: 'Express'
});
});
app.listen(3000);
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
And now the problem is, that if I revisit the page after 30 seconds, I get a new SID, but the old session in memorystore is still there... should it not be checked every 5 seconds and deleted?
Thx for your help!
So the first problem is a misunderstanding here. reapInterval does not do anything. MemoryStore clears cookies based on the expire time of session cookie. So there is in fact a bug in Connects MemoryStore. The way I see it the broken flow goes like this.
Set cookie to expire in X.
Get session data, has X gone by? No, ok.
(cookie expires)
Get session data, session doesn't exist, generate new one.
X has gone by but the session ID is missing because the browser expired it already.
There is a discussion regarding this here.
https://github.com/senchalabs/connect/issues/328
And a juicy quote
"short cookie sessions would be screwed I guess" --visionmedia