session icon indicating copy to clipboard operation
session copied to clipboard

Sessions In API's

Open wesleytodd opened this issue 9 years ago • 37 comments

Note: So I am sorry if this is a duplicate of something else, but most of the issues I found relating to this are not all that helpful.

Using the RedisStore, but this is not redis specific, I was planning on sharing my sessions between a bunch of API microservices and a web app. Obviously I don't want to use cookies for these api's, I want to use the auth header. But the same session will also be accessed from my web app, which will use cookies.

To solve this I would like to generate a single token to act as both my cookie value and my auth header. When my web app makes a request to an api it would just load the cookie value into the auth header.

At my work we have a session module that does this and it works great, but it is not open source. I was hoping to get your opinion on having this module support that. Do you like the idea? Do you know of another module that already does this? Is there a way to get this module to do that already that I am just stupidly missing? I would be happy to get this working in a PR or make a proof of concept first, just let me know.

wesleytodd avatar May 24 '15 01:05 wesleytodd

So, I looked into the code a bit, and it looks like we could pretty easily break the main body of this code out into two drivers, which would just be middleware generator functions:

  • Cookie: Gets and sets session in a cookie, basically the code that is here
  • Header: Gets from the header but provides a method for generating the token to return to the client on authentication

This is basically the architecture we use at work for our session module.

wesleytodd avatar May 24 '15 02:05 wesleytodd

https://github.com/wesleytodd/session/tree/api-sessions

I was pretty easily able to get the test passing after moving most of the login out into a driver. I'm going to try and get a header implementation working next.

Even if this doesn't look like something you guys would want to merge, I might maintain a fork, as I have needed something like this a few times in the past.

wesleytodd avatar May 24 '15 16:05 wesleytodd

Yes, this is desired. I'm currently out for the holiday weekend where I am, but I wanted to give you that feedback and let you know you're not being ignored :)

dougwilson avatar May 24 '15 18:05 dougwilson

Awesome! I just got it working in my api, it is very rudimentary, and to really get something good the entire module will need some attention. But on that branch I linked to above you can see my 2 commits.

The things that would need to change to make this implementation clean:

  • The req.session.cookie value is a misnomer in that the header driver just uses it for the maxAge stuff. Would be better to call it something like res.session.config.
  • The current header driver doesn't do any signing. My thought here is that it is not common practice to sign bearer tokens, they are just sent as is. Which makes the secret NOT required. This would probably need discussion.
  • The drivers would be better broken out into separate small modules. But right now they are way too specific. They could easily be made into more generic modules that other people could use as the basis for different session implementations.

Anyway, not all of that would be required, I mean i have it working right now on that branch. But it would be better than it is now if we did it.

wesleytodd avatar May 24 '15 18:05 wesleytodd

Any feedback on my progress there would be awesome. I will hopefully be able to do a bit more work on this soon, and would like to know first if you all like my direction. I can take a different direction if you have any better ideas!

wesleytodd avatar Jun 02 '15 22:06 wesleytodd

It sounds good :) There is a PR going on, it seems (#164) that seems kinda similar (?). How does your stuff compare to that PR? Why not just make a PR and we can discuss over code :)? The main desirable property right now is to be backwards-compatible, at least until August.

dougwilson avatar Jun 03 '15 03:06 dougwilson

Ill have a look at that tonight and let you know! I believe mine maintains backward compat fwiw.

wesleytodd avatar Jun 03 '15 14:06 wesleytodd

Is there any interest in this PR?

Let me expound on the situation here:

In a SOA environment, most html delivering "web clients" (aka front-ends), will only use the cookie as a pass through to the api layers. So I actually removed this middleware entirely from my client apps and ONLY pass the cookie value through as Authorization: bearer <COOKIE_VALUE> via the code in this PR.

The way you previously had to do this, with this middleware, was by parsing the cookies in your web client, then setting your sid cookie onto the api requests you were making behind the scenes. Then the api would also have to parse the cookies to get it out. The issue here is that for most situations, api's also have to support authorization headers because of things like native clients and other common best practices. Which complicates the api's a fair bit.

This PR should be fully backward compatible because the default driver is still the cookie, but driver option allows for users to set the header option to parse the session id value from the authorization header.

Since this PR is almost a year old, I realize that it needs some clean up, like adding documentation and removing the version bump that I added for some unknown reason :) But if you all like the looks of it I can do that clean up so we could maybe get this merged?

wesleytodd avatar Feb 20 '16 16:02 wesleytodd

I realized I didnt re-link to the PR, so here is the link to the actual code: https://github.com/expressjs/session/pull/170

wesleytodd avatar Feb 20 '16 16:02 wesleytodd

Is there a internal method to load session from its id? so, we can create a middleware that load the session based on a custom header (valorized with session id used as a session token).

daaru00 avatar Feb 27 '16 09:02 daaru00

Are you meaning like this? https://github.com/expressjs/session/blob/master/index.js#L405

#170 is doing what you are asking, "loading the session based on a custom header". This PR allows for passing the session id as an Authorization header or some other custom header you specify. Does that help?

If you are interested in this feature, please make some noise so we can make some traction on it.

wesleytodd avatar Feb 27 '16 15:02 wesleytodd

Are you meaning like this? https://github.com/expressjs/session/blob/master/index.js#L405

nearly, I mean something like this:

var session = require("express-session");
app.use(function(req, res, next) {
    var session_id = req.header("x-session-token");
    req.session = session.getById(session_id); //like this
    next(); //here session is loaded
});

This would lead to implement a session sharing across multiple devices:

var session = require("express-session");
app.use(function(req, res, next) {
    var token = req.header("x-session-token");
    var user_id = getUserIdFromToken(token); //my internal method
    var session_id = getUniqueSessionIdBasedOnUser(user_id); //my internal method
    req.session = session.getById(session_id);
    next(); //here session is loaded
});

this is only an idea but with a method like this I can implement it.

If you are interested in this feature, please make some noise so we can make some traction on it.

yes, I'm very interested, my problem would be resolved!

daaru00 avatar Feb 27 '16 17:02 daaru00

If #170 lands, you should be able to let this module do all that work for you, for example:

app.use(session({
  driver: 'Header',
  name: 'x-session-token'
}));

app.get('/', function (req, res) {
  if (req.session && req.session.userId) {
    console.log('user logged in');
  } else {
    console.log('no logged in user');
  }
});

And it will do exactly what you are doing where you could just access req.session. The Header driver defaults to loading from a standard bearer token format of Authorization: bearer <session_id>. But by overriding the name you can use whatever header you want.

wesleytodd avatar Feb 27 '16 17:02 wesleytodd

Exactly, I'll wait for this feature. :+1:

daaru00 avatar Feb 27 '16 17:02 daaru00

Still waiting :)

atharrr avatar Jun 19 '18 12:06 atharrr

me toooooo

xelaz avatar Apr 16 '21 11:04 xelaz

Would be really cool if this issue gets some attention. Using express-session in its current state is unhelpful with API's. Manufacturers are allowing cookies less and less to be used to maintain sessions and it's already completely abolished on both Apple iOS and newer Android's making it a big problem for having a mobile app that needs to login and communicate with a NodeJS backend.

ultimate-tester avatar Feb 13 '22 14:02 ultimate-tester

Related: https://github.com/expressjs/session/issues/567 https://github.com/expressjs/session/issues/543 https://github.com/expressjs/session/issues/317

Have something working with few lines of code on top of expressjs-session + redis. Wanted to get the community's thoughts on this approach. My requirement is that the same backend needs to be used for a mobile app and a web app. And on the mobile side, the app should never auto-logout. On the web app side, I need to auto-logout after 2 hours.

frontend

  • send x-platform = web OR ios OR android header
  • if mobile
    • store req.sessionID as accessToken in Expo's SecureStore
    • send Authorization: Bearer ${accessToken} header

account router

router.post('/login', async (req: Request, res: Response) => {
  const userInfo = await login({ ... });

  req.session.user = userInfo;

  res.json({
    ...userInfo,
    authToken: req.sessionID,
  });
});

middleware

import cs from 'cookie-signature';
...
app.use((req, res, next) => {
  const accessToken = req.get('authorization')?.split('Bearer ')[1];
  if (accessToken) {
    const final = `s:${cs.sign(accessToken, process.env.SESSION_SECRET!)}`;
    req.headers.cookie = `${process.env.SESSION_COOKIE_NAME}=${final};`;
  }

  next();
});

const RedisStore = connectRedis(session);
app.use((req, res, next) => {
  const middleware = session({
    name: process.env.SESSION_COOKIE_NAME,
    store: new RedisStore({
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT!),
      client: redisClient,
      logErrors: true,
      disableTTL: req.get('x-platform') !== 'web',
    }),
    secret: process.env.SESSION_SECRET!,
    cookie: {
      domain: blah.com,
      maxAge: req.get('x-platform') === 'web' ? 7200000 : undefined, // 2 hours for web
    },
    resave: false,
    saveUninitialized: false,
    rolling: true,
  });

  middleware(req, res, next);
});

admehta01 avatar Apr 17 '22 04:04 admehta01

Hey @admehta01 this look pretty good, except it looks like you're instantiating a new instance of the middleware for each request. Instead of that you would want to instantiate a "mobile" and "web" and then pass the request to the existing middleware. Example:

const RedisStore = connectRedis(session);
const webMiddleware = session({
    name: process.env.SESSION_COOKIE_NAME,
    store: new RedisStore({
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT!),
      client: redisClient,
      logErrors: true,
      disableTTL: false,
    }),
    secret: process.env.SESSION_SECRET!,
    cookie: {
      domain: blah.com,
      maxAge: 7200000, // 2 hours for web
    },
    resave: false,
    saveUninitialized: false,
    rolling: true,
  });

const mobMiddleware = session({
    name: process.env.SESSION_COOKIE_NAME,
    store: new RedisStore({
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT!),
      client: redisClient,
      logErrors: true,
      disableTTL: true,
    }),
    secret: process.env.SESSION_SECRET!,
    cookie: {
      domain: blah.com
    },
    resave: false,
    saveUninitialized: false,
    rolling: true,
});

app.use((req, res, next) => {
  if (req.get('x-platform') === 'web' ) {
    return webMiddleware(req, res, next);
  }

  mobMiddleware(req, res, next);
});

joewagner avatar Apr 17 '22 15:04 joewagner

With the help of @admehta01 's comment, I am able to authenticate clients with http Authorization header. One another undesirable thing is that the Set-Cookie header is always sent to the client. How do I avoid sending Set-Cookie header?

Edit: I made an option to optionally send Set-Cookie to the client in https://github.com/contrun/express-session/tree/optionally-set-Set-Cookie .

contrun avatar Jan 06 '24 15:01 contrun

In the end my approach was the following:

In successful login, share the session ID with the client through JSON. It will be remembered by the client. Then, I created a middleware that is in the chain right before the session middleware. This custom middleware simply takes a header value (x-session-id), converts it into a connect.sid value and places that into the cookies object of the request. This way when the session middleware runs, it will pick up the connect.sid cookie value that was placed and restore the session properly without any side effects.

Only challenge was to create the appropriate value, but the 'sign' method of the 'cookie-signature' package will help you with that (use the same secret as you gave to the session middleware!) I believe the structure of the connect.sid cookie value is:

s:<signed session ID>

If anyone is interested I can publish this middleware as a package on NPM, just let me know.

ultimate-tester avatar Jan 06 '24 16:01 ultimate-tester

@ultimate-tester Thank you for sharing your insights. I think what you did matches https://github.com/expressjs/session/issues/161#issuecomment-1100803240 perfectly. Don't you have the problem of sending the Set-Cookie header to the client? How did you avoid that or did you just ignore it?

contrun avatar Jan 06 '24 16:01 contrun

Ah yes that looks quite similar indeed. The idea is that you send the session ID as JSON body to the client (only once at authentication), not through cookies anymore. I left the cookie sending in place though (this is core functionality of the session module), but it's ignored on the mobile clients. Nowadays even on my desktop clients I use the header rather than cookie using this custom middleware to keep a uniform approach.

ultimate-tester avatar Jan 06 '24 16:01 ultimate-tester

Hey @admehta01 this look pretty good, except it looks like you're instantiating a new instance of the middleware for each request. Instead of that you would want to instantiate a "mobile" and "web" and then pass the request to the existing middleware. Example:

const RedisStore = connectRedis(session);
const webMiddleware = session({
    name: process.env.SESSION_COOKIE_NAME,
    store: new RedisStore({
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT!),
      client: redisClient,
      logErrors: true,
      disableTTL: false,
    }),
    secret: process.env.SESSION_SECRET!,
    cookie: {
      domain: blah.com,
      maxAge: 7200000, // 2 hours for web
    },
    resave: false,
    saveUninitialized: false,
    rolling: true,
  });

const mobMiddleware = session({
    name: process.env.SESSION_COOKIE_NAME,
    store: new RedisStore({
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT!),
      client: redisClient,
      logErrors: true,
      disableTTL: true,
    }),
    secret: process.env.SESSION_SECRET!,
    cookie: {
      domain: blah.com
    },
    resave: false,
    saveUninitialized: false,
    rolling: true,
});

app.use((req, res, next) => {
  if (req.get('x-platform') === 'web' ) {
    return webMiddleware(req, res, next);
  }

  mobMiddleware(req, res, next);
});

@joewagner This does not seem to work. My requirement is to have dynamic cookie.domain based on req.headers.host and I thought I could just create 1 session middleware per host in advance and then just reuse correct one at runtime. After some debugging I found out latest session() invocation always replaces previous ones. I.e if i have set it up 3 times, only the last configuration is active, seemingly overwriting previous ones.

dfenerski avatar Jan 14 '24 08:01 dfenerski

Hey @admehta01 this look pretty good, except it looks like you're instantiating a new instance of the middleware for each request. Instead of that you would want to instantiate a "mobile" and "web" and then pass the request to the existing middleware. Example:

const RedisStore = connectRedis(session);

const webMiddleware = session({

name: process.env.SESSION_COOKIE_NAME,
store: new RedisStore({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT!),
  client: redisClient,
  logErrors: true,
  disableTTL: false,
}),
secret: process.env.SESSION_SECRET!,
cookie: {
  domain: blah.com,
  maxAge: 7200000, // 2 hours for web
},
resave: false,
saveUninitialized: false,
rolling: true,

});

const mobMiddleware = session({

name: process.env.SESSION_COOKIE_NAME,
store: new RedisStore({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT!),
  client: redisClient,
  logErrors: true,
  disableTTL: true,
}),
secret: process.env.SESSION_SECRET!,
cookie: {
  domain: blah.com
},
resave: false,
saveUninitialized: false,
rolling: true,

});

app.use((req, res, next) => {

if (req.get('x-platform') === 'web' ) {

return webMiddleware(req, res, next);

}

mobMiddleware(req, res, next);

});

@joewagner This does not seem to work. My requirement is to have dynamic cookie.domain based on req.headers.host and I thought I could just create 1 session middleware per host in advance and then just reuse correct one at runtime. After some debugging I found out latest session() invocation always replaces previous ones. I.e if i have set it up 3 times, only the last configuration is active, seemingly overwriting previous ones.

Try @brevisstudios/session-from-header package.

ultimate-tester avatar Jan 14 '24 08:01 ultimate-tester

@dfenerski It sounds like you might have configured something wrong, can you share a repo or gist with the related code?

joewagner avatar Jan 14 '24 17:01 joewagner

Sure @joewagner, here is a repo where the issue is reproducible: https://github.com/dfenerski/express-double-session-issue.

dfenerski avatar Jan 21 '24 14:01 dfenerski

Looks like only session1 is ever used, and it is applied unconditionally to all requests: https://github.com/dfenerski/express-double-session-issue/blob/d04b2fa1f43b359ce195b52ba1b3f8d919e9063f/app.js#L47

dougwilson avatar Jan 21 '24 14:01 dougwilson

Yes, that's the point. You might think the domain of session1 is set in the cookie, but its the one of session2 instead :) Seemingly overwriting, as per my initial comment

dfenerski avatar Jan 21 '24 14:01 dfenerski

Oh, I see. Sorry, there is a lot going on in this issue to resd through so I missed that. Is this an issue with using a session in APIs or something else?

dougwilson avatar Jan 21 '24 14:01 dougwilson