Express-session with passport.js multiple cookies - node.js

I'm trying to implement the login functionality with passport.js and express-session, it works well,
var passport = require('passport'),
session = require('express-session'),
MongoDBStore = require('connect-mongodb-session')(session);
var sessionStore = new MongoDBStore({
uri: config.db,
collection: 'sessions'
});
var sessionOptions = {
name: 'very-secure-cookie',
secret: config.secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: true,
maxAge: null
},
store: sessionStore
};
app.use(session(sessionOptions));
app.use(passport.initialize());
app.use(passport.session());
Here, maxAge is null, meaning that the client gets logged out when they close the browser window. However, I want to store an additional cookie with the client's email address in order to pre-fill the <input type="text" name="email"> when they want to log back in if they come later.
I've tried app.use()'ing another session object just to store the email information in another cookie, but for some reason the cookie is set only once (the first one).
app.use(session(returningSessionOptions));
Couldn't find any sensible solution anywhere :(
Oh, and I have to mention that I thought about using cookie-parser middleware alongside with express-session, but the docs state that it may result in conflicts.

As you didn't include the complete code you used, I assume you set the cookie using res.cookie(name, value[, options]); or res.setHeader('set-cookie', serializedCookie);.
There seems to be an issue with the set-cookie header. When adding multiple cookies, they are appended to the header, separated by comma, which doesn't seem to work in most current browsers. The header is only interpreted until the first comma and therefore only the first cookie is saved. In some browsers everything between the first equals sign and the following semicolon is used as value for that first cookie.
Possible solution
I could reproduce the issue with an old version of Express (3.0.0) using the code below. Using Express 4.13.4 (and also 3.21.2) the same code worked fine. If you are working with an old version of Express, I recommend that you update to the latest one as this should fix the problem. If you are already using a current version, please try if the example works for you. If not, please provide the code and the headers sent by your express-app. In some cases with a more complex application the problem exists even when using versions that work with the example code (I tried Express 3.21.2).
If there is any reason for you not to update to the latest version or the update doesn't fix the issue for you, there is an alternative solution for the problem:
Code to reproduce the issue
To eliminate possible side-effects by other code in the application, here is the minimal example that I used to reproduce your issue:
var express = require('express');
var session = require('express-session');
var passport = require('passport');
var app = express();
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
app.get('/', function (req, res) {
res.cookie('cookie', 'value');
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
When accessing the express-app (running Express 3.0.x) via the browser, the following response headers are sent:
set-cookie: cookie=value; Path=/
set-cookie: cookie=value; Path=/,connect.sid=s%3ARPkxiLfy0dbeBs5eILYBLK6Yh-sOjABS.ykNXCmaOsxtIAXV%2Ba9GCW6YnBi0cBhvZIGvwlj%2F%2Fsy0; Path=/; HttpOnly
Using Express >=3.21.2 everything works fine for this example:
set-cookie: cookie=value; Path=/
set-cookie: connect.sid=s%3AR6TVTnUe3eyIvlpT0Hl5ikwcH_XMHXId.ouysyG5tGGekaVDxZMXJP4A8SJfsckLE4GZ3%2B1Eyd1o; Path=/; HttpOnly
Alternative fix
Borrowing the code of function setcookie from express-session for setting your cookies could do the trick for you, as it writes an array containing all the cookies to the header while preserving the ones already added to the response. To do this, install the cookie module, add var cookie = require('cookie'); and replace res.cookie('cookie', 'value'); in the above code with the following lines:
var data = cookie.serialize('cookie', 'value');
var prev = res.getHeader('set-cookie') || [];
var header = Array.isArray(prev) ? prev.concat(data)
: Array.isArray(data) ? [prev].concat(data)
: [prev, data];
res.setHeader('set-cookie', header);
Now the following headers are sent to the browser and all cookies are saved (tested using Google Chrome 49 on MacOS):
set-cookie:cookie=value
set-cookie:cookie=value
set-cookie:connect.sid=s%3Aa1ZPDmERtKwUaPjY__SrPtIrpYC7swQl.0KOs83RSmUTG%2FgPoyOLo4u5UFTjC89yS0Ch0ZVXWVo8; Path=/; HttpOnly
Unfortunately the header is duplicated for the first cookie when using Express 3.0.0, which shouldn't be done according to RFC6265 Section 4.1.1:
Servers SHOULD NOT include more than one Set-Cookie header field in
the same response with the same cookie-name. (See Section 5.2 for
how user agents handle this case.)
Helpful resources
The following articles may also be helpful:
Someone facing a similar problem, but with ASP.NET/Web API instead of node.js/express. However, in my opinion this page provides helpful information on the topic: Set-Cookie Header With Multiple Cookies
Another possible solution for setting multiple cookies with node.js/express that might help: http://www.connecto.io/blog/nodejs-express-how-to-set-multiple-cookies-in-the-same-response-object/

Related

Nextjs & custom Express server - cannot access cookie set with `http: true` in the express server [duplicate]

This question already has answers here:
How to send cookies with node-fetch?
(3 answers)
Why are cookies not sent to the server via getServerSideProps in Next.js?
(1 answer)
Closed 1 year ago.
I am trying to parse a cookie which was set by including the property httpOnly: true on my custom express server
Now in my NextJs app I can fetch the cookie in the server side method getServerSideProps and access the cookie in ctx.req.cookies from there. But when I make a fetch api call (from the server side method) to the custom server, the cookie does not seem to be accessible in the custom server api call.
Here is my express configuration in server.ts:
// Express Configuration
app.set('port', process.env.PORT || 3000)
app.use(cors({ credentials: true, origin: true }))
app.use(cookieParser())
app.use(passport.initialize())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
This is how I set the cookie on custom express server:
res.cookie('rtk', refreshTokenJWT.token, {
httpOnly: true,
})
This is the fetch api call from getServerSideProps method:
const response = await fetch('http://localhost:3000/refresh_token', {
credentials: 'include',
method: 'POST',
})
Perhaps it is because of the fact that I have to make the api call with absolute url, when I try to do fetch('/refresh_token', ...) I get the error: TypeError: Only absolute URLs are supported
I could possibly send the payload from the server side method in the request body and handle it in the custom express server, but this does not seem like the best solution.
pls elp, thanks in advance.
Edit: To clarify, the await fetch() is happening in the server side method getServerSideProps of nextjs.
Sending the cookie as a header in the fetch api call as suggested by #O. Jones works, but I was under the impression that this wouldn't work since I had set httpOnly: true.
You have to, and are allowed to, parse that server response header despite the httponly attribute being present.
Why? Because HttpOnly (owasp link) is only an instruction to the browser that it should enforce that isolation; when your client code is just another node.js process, it's just a response header.
Here's the regurgitation I'm doing in my auth's unit testing that sounds very similar to your needs; I'm always getting exactly two cookies in comma-delimited one liner format, as emitted by my Koa RESTful app (your Express setup could be comma-delimited like this one, or send two Set-Cookies, so you may need a minor alteration if you have the latter case.)
// raw's format is `koa.sess=YA5N/yI1KhKc/qyylgNduj8vK3e2; path=/; expires=Sun, 09 May 2021 05:17:19 GMT; secure; httponly, koa.sess.sig=8x1BraqjAvKryLx1fvgc0DBu5D4; path=/; expires=Sun, 09 May 2021 05:17:19 GMT; secure; httponly`;
const scp = require('set-cookie-parser');
const cookies = scp.parse(scp.splitCookiesString(headers['set-cookie']));
const next_request_headers = {
cookie: cookies.map(cookie => cookie.name + '=' + cookie.value).join('; ')
};
I'm using npm:set-cookie-parser because cookies are jank and I got fed up with handling commas in both expirations and delimiters.
side issue: abs URLs
TypeError: Only absolute URLs are supported
Cookie domain rules are applied after relative URLs are normalized to absolute URLs, so it sounds to me like this side-issue is only about node-style fetches not having the implicit server/path needed for a browser-style relative ajax URL, and not blocking on anything to do with cookies.
getServerSideProps, by it's name, means it's generated server side
When you do a fetch in getServerSideProps, you do not have any default cookies.

Why are my express sessions not persisting and why am I getting a new session (or multiple sessions) created for each request

I have a basic node.js express app using express-sessions.
Please can someone help with why the sessions are not persisting and why a new session is created for every request.
The app itself is quite large so i have added a reduced case of the important settings below.
const express = require('express');
const session = require('express-session');
const MongoDBStore = require('connect-mongodb-session')(session);
// initialise express
const app = express();
// initialise db session store
const store = new MongoDBStore({
uri: MONGODB_URI,
collection: 'sessions',
// can also set expire here for auto cleanup by mongodb
});
app.use(session({
secret: 'secret password',
resave: false,
saveUninitialized: false,
httpOnly: false,
secure: app.get('env') === 'production',
store,
}));
...routes
in a user login route the app sets the request user to the session and saves the session. it is expected that this req,session.user will persist between pages but it does not. on each page request i can see a new session (or sometimes multiple sessions, 1 for each file request) being created.
UPDATE
TL:DR;
- robots.txt causes issues if not dealt with, set DEBUG env to express-session to troubleshoot
After a lot of hair pulling I've found a solution and some useful troubleshooting tips.
when running your app, run it with debug set to express-session.
so for those of you that are quite new to this like myself, run your app with a command similar to this:
DEBUG=express-session node 'bin/www.js'
or
DEBUG=express-session node app.js
depending on how you have your app entry point setup.
Doing this will print session related log msgs so you can troubleshoot if the cookie is actually getting sent with each request or not. the error messages will look like something this:
express-session fetching 0wgmO1264PsVvqeLqaIIXd6T0ink0zts +34s
express-session session found +49ms
To troubleshoot the issue of multiple requests causing multiple sessions per page load, Add a middleware at the top of your app before any other middleware. this will allow us to see the request URL and troubleshoot which requests may be interfering with our sessions.
// see what requests are being sent and which ones contain cookies
app.use((req, res, next) => {
const { url } = req;
const isCookieSent = req.headers.cookie;
console.log({ url });
console.log({ isCookieSent });
next();
});
from doing this I found out that the culprit was robots.txt file, Apparently the only path that is ignored by default is favicon.ico.
Because this robots.txt path wasn't handled properly, nor was it sending a cookie, it was causing the duplicate requests and also causing the cookies not to persist.
to fix this you either need to handle or ignore this request prior to getting to the session middleware.
i did this using this middleware, once again fairly high up.
app.get('/robots.txt', (req, res) => {
res.type('text/plain');
res.send('User-agent: *\nDisallow: /');
});
I am new to node.js so if there is anyone with more knowledge feel free to chip in with extra info or cleaner ways of solving this problem. Hopefully this saves some of you a lot of hassle!

Express session-cookie not being sent on openshift with https and secure flag

Got a strange issue, I am using Express and in development we use http and have secure: false for the session cookie, however now we are moving to openshift we have turned https on thinking it would be a simple endeavour but our cookies are not being sent back with the responses. If however we turn off https and revert back to http on openshift it works fine and cookies are sent.
So here is an example of what the cookie config looks like:
var setupSession = function() {
var sessionConfig = {
secret: environmentVars.cookie.secret,
name: environmentVars.cookie.name,
maxAge: environmentVars.cookie.expiry,
domain: environmentVars.cookie.domain,
httpOnly: true,
secure: environmentVars.cookie.secure, // true when using https
secureProxy: environmentVars.cookie.secure, // true when using https
signed: true
};
app.set('trust proxy', 1); // Just added this, still no luck
app.use(session(sessionConfig));
};
So the above is run when the app starts up and as noted in the comments when we are using a secure connection the environment vars are set for us, and when the above is used in conjunction with HTTPS no cookie is sent back from express, however openshift cookies are sent back, like the gears one etc. Again with http and disabling the secure stuff it works fine we all get cookies and rejoice. All responses work and data is sent back its just the set-cookie header is missing for the apps cookies (but as mentioned not openshift ones).
So the actual certificate is not setup within nodejs it is setup on openshift as an alias with a certificate applied. So express really has no idea it is being run in https other than the environmental vars it is passed and the port it is provided by the gear that is running it.
So has anyone else had anything similar or has any ideas on what we can try to solve the problem or diagnose it? I did some reading and people suggested trying the trust proxy and secureProxy, which has been done but still no luck.
So it turns out I was just being an idiot, it should look like:
var setupSession = function() {
var sessionConfig = {
secret: environmentVars.cookie.secret,
name: environmentVars.cookie.name,
maxAge: environmentVars.cookie.expiry,
domain: environmentVars.cookie.domain,
httpOnly: true,
secureProxy: environmentVars.cookie.secure, // true when using https
signed: true,
cookie: {
secure: environmentVars.cookie.secure, // true when using https
}
};
app.set('trust proxy', 1); // Just added this, still no luck
app.use(session(sessionConfig));
};
All works now :)
I had similar problem with express-session and after many trials the culprit for me was setting cookie.domain. Browsers wouldn't save the cookie.
This is how I was setting the value:
cookie: {
...
domain: process.env.OPENSHIFT_CLOUD_DOMAIN,
...
}
Hope it helps anyone going through the same, since at the time this is the best suited stackoverflow question to share this.

CSRF token issues with Express

I am trying to get CSRF protection working using csurf and express. My app uses Angular for the front end, so I figured adding this to my app would be enough:
app.use(cookieParser('test secret'));
app.use(cookieSession({
secret: 'test secret'
}));
app.use(csrf({
cookie: {
key: 'XSRF-TOKEN'
}
}));
However, when I try to make POST requests, I get 403 ("invalid CSRF token") errors.
As far as I can tell, the issue resides in the csrf-tokens module: at line 44, the expected variable looks like qMnHLQGhivxECx5WtwuktDNA-snimacq30z-XNh2X-KTpdlkU6Og while the secret and token variables look like qMnHLQGhivxECx5WtwuktDNA. Since expected and token are different, a 403 error occurs.
I have, so far, tried the following:
Setting secure: false and/or signed: true in my csurf options
Making sure that my cookie-session and cookie-parser middleware run before csurf
Re-installing all my npm modules just in case
Yelling at my computer
I could, potentially, switch to session-based tokens, but that wouldn't work as smoothly with Angular, and I'm not sure if it would even solve my problem.
Does anyone know how to fix this issue, or what the cause might be?
P.S. By the way, I've also noticed a (probably unrelated) issue: the req.csrfToken() method seems to return strings whose values are entirely unrelated to the expected value in csrf-tokens or to the string being stored in the cookie.
There is an error/bad implementation regarding csurf lib. The cookie "XSRF-TOKEN" does not store the csrf Token, but the secret that csurf uses to generate the csrf token. Therefore, the Angular part of your app identifies the XSRF-TOKEN and appends the header correctly to HTTP requests, but the csurf middleware does not recognize the secret as a valid CSRF token itself. To fix this problem, you should implement this as:
var cookieParser = require('cookie-parser');
var cookieSession = require('cookie-session');
app.use(cookieParser("secret"));
app.use(cookieSession({secret: "secret2"}));
app.use(csrf());
app.use(function (req, res, next) {
res.cookie("XSRF-TOKEN",req.csrfToken());
return next();
});
The req.csrfToken() will generate the csrf token based on the secret stored, in this case, in the req.session.csrfSecret variable, generated by the csrf middleware. Then, it will work.
EDIT: See this issue on GitHub

CouchDB with NodeJS authentication solution

In node.js my auth library (lockit) is not setting session after logging in successfully. (Disclaimer: I am not condoning the use of lockit, I think it is positively terrible in every way.)
I read lockit source code and found this code:
// create session and save the name and email address
req.session.name = user.name;
req.session.email = user.email;
Here is my express config:
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(cookieSession({
secret: 'this is my super secret string'
}));
app.use(lockit.router);
After I login with a user, I send another request with the same browser and the cookie is the same that gets set from the login request...
After successful login here is the Set-Cookies I get:
Set-Cookie express:sess=eyJmYWlsZWRMb2dpbkF0dGVtcHRzIjowLCJuYW1lIjoianVzdGluIiwiZW1haWwiOiJqdXN0aW5Ad2ViaW52ZXJ0ZXJzLmNvbSIsImxvZ2dlZEluIjp0cnVlfQ==; path=/; httponly
Set-Cookie express:sess.sig=FMcv9fswWmWG6A7hpOEnEysbqd4; path=/; httponly
Then my request after the login contains these cookies:
express:sess eyJmYWlsZWRMb2dpbkF0dGVtcHRzIjowLCJuYW1lIjoianVzdGluIiwiZW1haWwiOiJqdXN0aW5Ad2ViaW52ZXJ0ZXJzLmNvbSIsImxvZ2dlZEluIjp0cnVlfQ==
express:sess.sig FMcv9fswWmWG6A7hpOEnEysbqd4
But even though the cookie is there, I'm getting:
req.session === undefined
I've never used these technologies before, so it could be something really stupid that I am doing...
I'm the author of Lockit and I'm sure we will find a solution to your problem. Did you go through all the required steps to install Lockit? Here is what I just did and it works fine.
Create a new Express app (Express 4.2.0 in my case).
express
Install Express dependencies.
npm install
Install Lockit and Sessions.
npm install lockit cookie-session --save
Install CouchDB adapter.
npm install lockit-couchdb-adapter --save
Create a config.js with the URL of your CouchDB
// settings for local CouchDB
exports.db = 'http://127.0.0.1:5984/';
Initiate Lockit with your config.
// in your header
var cookieSession = require('cookie-session');
var Lockit = require('lockit');
var config = require('./config.js');
var lockit = new Lockit(config);
// after all your other middleware
app.use(cookieSession({
secret: 'my super secret String'
}));
app.use(lockit.router);
Start your app.
DEBUG=tmp ./bin/www
You can now navigate to http://localhost:3000/signup and create a new user. If you haven't set up any email server you have to look in your database (in my case at http://127.0.0.1:5984/_utils/database.html?_users) for your signup token. Then go to http://localhost:3000/signup/:token to activate your new user.
Great, you can now use your username (or email) and password to log in. To access the currently logged in user in your own routes use req.session.
// default routes from Express
app.use('/', routes);
app.use('/users', users);
// your own custom route
app.get('/awesome', function(req, res) {
console.log(req.session.name);
console.log(req.session.email);
res.send('awesome');
});
I hope I could help you. If you've got any other problems just let me know.
http://localhost:3000/signup/:token doesn't work, must be
http://localhost:3000/signup/token (without the column)

Resources