Node with Express session issue

Viewed 4389

I use the following code which works, However after a few success calls (5-10), we sometimes get an internal server error:

req.session["oidc:accounts.rvm.com"] is undefined

I've tried all the latest open source versions.

Error: did not find expected authorization request details in session, req.session["oidc:accounts.rvm.com"] is undefined
at /opt/node_app/app/node_modules/openid-client/lib/passport_strategy.js:125:13
at OpenIDConnectStrategy.authenticate (/opt/node_app/app/node_modules/openid-client/lib/passport_strategy.js:173:5)
at attempt (/opt/node_app/app/node_modules/passport/lib/middleware/authenticate.js:366:16)
at authenticate (/opt/node_app/app/node_modules/passport/lib/middleware/authenticate.js:367:7)
at /opt/node_app/app/src/logon.js:92:7 *******
at Layer.handle [as handle_request] (/opt/node_app/app/node_modules/express/lib/router/layer.js:95:5)
at next (/opt/node_app/app/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/opt/node_app/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/opt/node_app/app/node_modules/express/lib/router/layer.js:95:5)
at /opt/node_app/app/node_modules/express/lib/router/index.js:281:22

My code from the stack is:

at /opt/node_app/app/src/logon.js:92:7

Which is the end of the code here:

})(req, res, next);   // here is line 92 but not sure if it's related 

This is the full code (I pass the app which is simply an express server):

index.js

const express = require('express');
const logon = require('./logon');

const app = express();
const port = process.env.PORT || 4000;

logon(app)
  .then(() => {
    console.log('process started');
  });
app.use(express.json());

app.listen(port,
  () => console.log(`listening on port: ${port}`));

logon.js

const { Issuer, Strategy } = require('openid-client');
const cookieParser = require('cookie-parser');
const cookieSession = require('cookie-session');
const azpi = require('./azpi');
const bodyParser = require('body-parser');
const passport = require('passport');

module.exports = async (app) => {
  let oSrv;
  const durl = `${process.env.srvurl}/.well-known/openid-configuration`;
  try {
    oSrv = await Issuer.discover(durl);
  } catch (err) {
    console.log('error occured', err);
    return;
  }

  app.get('/', prs(), passport.authenticate('oidc'));

  const oSrvCli = new oSrv.Client({
    client_id: process.env.ci,
    client_secret: process.env.cs,
    token_endpoint_auth_method: 'client_secret_basic',
  });

  passport.serializeUser((user, done) => {
    done(null, user);
  });
  passport.deserializeUser((obj, done) => {
    done(null, obj);
  });

  const cfg = {
    scope: 'openid',
    redirect_uri: process.env.ruri,
    response_type: 'code',
    response_mode: 'form_post',
  };

  const prs = () => (req, res, next) => {
    passport.use(
      'oidc',
      new Strategy({ oSrvCli , cfg }, (tokenset, done) => {
        const claims = tokenset.claims();
        // first log
        console.log(`1. ------------User claims received------------);
        const user = {
          name: claims.name,
          id: claims.sub,
          id_token: tokenset.id_token,
        };
        return done(null, user);
      }),
    );
    next();
  };
  app.use(
    bodyParser.urlencoded({
      extended: false,
    }),
  );
  app.use(cookieParser('csec'));
  app.use(
    cookieSession({
      name: 'zta-auth',
      secret: 'csect',
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());

  app.get('/redirect', async (req, res, next) => {
    await passport.authenticate('oidc', async (err, user) => {
    // print second log
    console.log('2. ------------redirect Called!------------');
      if (err) {
        console.log(`Authentication failed: ${err}`);
        return next(err);
      }
      if (!user) {
        return res.send('no identity');
      }

      req.login(user, async (e) => {
        if (e) {
          console.log('not able to login', e);
          return next(e);
        }
        try {
          const url = await azpi.GetUsers(user.id_token);
          // print last log
          console.log('3. ------------user process finished successfully----');
          return res.redirect(url);
          
        } catch (er) {
          res.send(er.message);
        }
      });
    })(req, res, next);   //here is the error
  });
};

Sometimes when I debug, I see that the function is running out from GetUsers which is an async function and stops in })(req, res, next);, maybe it's an async issue.

We want to use this code in prod instead of the previous Java implementation.

If I can use another technique for oidc, please let me know.


UPDATE

Each should be a single call and log in this order:

1. ------------User claims received------------
2. ------------redirect Called!------------
3. ------------user process finished successfully----

But, when I get the error:

1. ------------User claims received------------
2. ------------redirect Called!------------
3. ------------user process finished successfully----

2. ------------redirect Called!------------
Authentication failed: Error: did not find expected authorization request details in session, req.session

All the successful calls have the right log order (1-3).

When it fails, the first call User claims received doesn't happen, just the second and the error.

If there is another way to achieve this (other lib's etc), please let me know.


I've found this library which may help as it doesn't use passport (I want to reduce deps to see where the problem is coming from).

When I try something like this:

app.use(
    auth({
     issuerBaseURL: `${URL}/.well-known/openid-configuration`,
     authorizationParams: {
    ...
     response_mode: 'form_post',
    }

I get this error: issuer response_mode supporting only "query" or "fragment", but when I run the code above (in the beginning of the post) with the same issuer and response_mode, everything is working, any ideas?

5 Answers

The issue appears to be a race condition whereby if you happen to get two requests, in-flight, at the same time, when one finishes it is clearing the session cookie before the other has the chance to complete. For what it's worth, you aren't the only person with this this problem.

I don't think it's an issue with the library itself though, I think the problem lies more with the session library. You may want to try express-session library with the saveUninitialized / resave options set to false and check whether you still see the same issue e.g.

const session = require('express-session');
...
app.use(session({
  saveUninitialized: false,
  resave: false
});

The only difference between this library and the cookie-session one you are using is express-session only stores the session ID in a cookie, the data is stored server-side. If you find it works, then you can look at using a more production-grade store (the default setting is to run with an in-memory one).

FWIW - you only need to configure the strategy one time, looking at what it does, I'd be surprised if it's part of the problem but I'd fix that just incase

We faced a similar issue but we had a more intermittent behavior, we had the error when logging in on Safari but not on Chrome.

From what I understood, this is because the session cookie is getting set when we are first authenticating, which stores the state, code-verifier (only if using PKCE flow) and other values the OIDC client needs to validate the authentication.

However, when the /callback URL is hit, the browser sends this session cookie to the server to complete authentication.

Whenever this cookie is not sent, thats when this error occurs, because the callback assumes this to be a new request and it crashes...

for us this was behaving in 2 ways.

  1. Cookie { Same-site: 'Lax' secure: true }

    Works for chrome but this did not work on safari

  2. Cookie { Same-site: 'None' secure: true }

    Works for chrome and on safari

this needs to be set on the express-session middleware (sorry Im not sure of the syntax thats needed)

One thought is if you decide to enable sessions, then you need to use express.session() before passport.session() to ensure that the user's login session is restored in the right order.

See this article

Use of waterfall async function may be helpful in this.Just replace app.get route function to below code. When we have to run tasks which depend on the output of the previous task, Waterfall can be helpful.

    app.get('/redirect', async (req, res, next) => {
        await passport.authenticate('oidc',
            async.waterfall([
                function (err,user) {
                    // print second log
                    console.log('2. ------------redirect Called!------------');
                    if (err) {
                        console.log(`Authentication failed: ${err}`);
                        return next(err);
                    }
                    if (!user) {
                        return res.send('no identity');
                    }

                    req.login(user, async (e) => {
                        if (e) {
                            console.log('not able to login', e);
                            return next(e);
                        }
                        try {
                            const url = await azpi.GetUsers(user.id_token);
                            // print last log
                            console.log('3. ------------user process finished successfully----');
                            return res.redirect(url);

                        } catch (er) {
                            res.send(er.message);
                        }
                    });
                }
            ], function (err) {
                if (err) return next(err);  //here you can check error
            })   
        );
    });

try use as this way

const key = crypto.randomBytes(16)
const mac = crypto.createMac('cmac', key, { cipher: 'aes-128-cbc' })
mac.update(crypto.randomBytes(30))
console.log(mac.final())
// => <Buffer b9>

Related