Session: Session Not Accessible After OAuth Redirect Back To App

Created on 16 Sep 2019  路  12Comments  路  Source: expressjs/session

Summary:
I'm trying to access some data written to a session on a previous route on another route. However, the session continuously comes back undefined despite it being visible in my Redis store. The route I am trying to access this session on is actually when an OAuth2.0 redirect takes place back to my app.

Workflow:

  • Click a link in my React front-end which redirects to a route on my Node backend
  • That route then creates a GUID state variable, saves it to the current session (this is done successfully) and redirects to the third party OAuth page
  • I sign into this said third party, which then redirects back to a route on my Node backend and checks the state matches the state saved to the session in the previous route
  • This session is always undefined despite it being visible in Redis

Technologies Used:

  • React (v16.9.0)
  • Node.js (v10.14.2)
  • Passport.js (v0.4.0)
  • Express (v4.17.1)
  • Express Session (v1.16.2)

Worth Noting:

  • This only in a development environment and nowhere near production ready.
  • As the third party I am using requires the redirect URL to not be a local one, I use ngrok to tunnel into the backend of the app.

Code Snippets:
_app.js_
```const bodyParser = require('body-parser');
const config = require('config');
const cors = require('cors');
const express = require('express');
const session = require('express-session')
const helmet = require('helmet');
const morgan = require('morgan');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const redis = require('redis');
const RedisStore = require('connect-redis')(session)
const uuidv1 = require('uuid/v1');
const uuidv4 = require('uuid/v4');

const helpers = require('./helpers');
const log = require('./logger');
const routes = require('./routes');

const app = express();
const redisClient = redis.createClient();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors({ credentials: true, origin: true }));
app.use(helmet());

app.use(session({
genid: () => {
return uuidv4(); // use UUIDs for session IDs
},
name: 'CustomName',
resave: false,
saveUninitialized: false,
secret: config.get('session.secret'),
store: new RedisStore({
client: redisClient,
prefix: 'custom-name-session-'
})
}));

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

morgan.token('reqid', req => req.reqid);

app.use((req, res, next) => {
req.reqid = uuidv1();
res.set('X-Request-Id', req.reqid);

next();
});

app.use(
morgan(
":remote-addr - - ':method :url HTTP/:http-version' " +
":status :res[content-length] :response-time ':reqid'",
{ stream: log.stream }
)
);

passport.use(new LocalStrategy(async (username, password, done) => {
let user;

try {
user = await helpers.findUser(username);

const message = 'Incorrect Credentials!';

if (!user) {
  return done(null, false, {message: message});
}

if (password !== user.password) {
  return done(null, false, {message: message})
}

}
catch (err) {
return done(err);
};

return done(null, user);
}));

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

passport.deserializeUser(async (id, done) => {
const user = await helpers.findUserById(id);
done(null, user);
});

app.use('/api', routes);

const port = process.env.PORT || 3001;

app.listen(port, () => log.info(Server started: http://localhost:${port}/));

_login.js_

const passport = require('passport');
const router = require('express').Router();

const log = require('../logger');

router.post('/login', (req, res, next) => {
console.log(req.headers);
passport.authenticate('local', (err, user, info) => {
if (err) {
log.error(err);
return next(err);
}

if (!user) {
  log.info(info);

  info.success = 0;
  return res.send(info);
}

req.logIn(user, (err) => {
  if (err) {
    log.error(err);
    return next(err);
  }

  return res.send({
    success: 1,
    message: 'Successfully logged in!',
    user: req.user
  });
});

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

router.get('/current-user', (req, res) => {
if (req.user) {
res.send({ user: req.user });
}
else {
res.send({ user: null });
}
});

router.get('/logout', (req, res) => {
req.logOut();
res.send({ success: 1, message: 'Successfully logged out!' });
});

module.exports = router;

_thirdParty.js_

const config = require('config');
const router = require('express').Router();
const uuid = require('uuid/v1');

const thirdParty = require('../../thirdPartyAPI');

router.get('/oauth/login', (req, res) => {
const ouathState = uuid();
req.session.ouathState = ouathState;

const thirdPartyConfig = config.get('thirdParty');
const clientId = thirdPartyConfig.clientId;
const redirectUrl = thirdPartyConfig.redirectUrl;

const thirdPartyOauthUrl = thirdPartyConfig.oauthUrl;

const url = ${thirdPartyOauthUrl}client_id=${clientId}&response_type=code&state=${ouathState}&redirect_uri=${redirectUrl};

res.redirect(url);
});

router.get('/oauth/redirect', async (req, res) => {
// const state = req.query.state;
const code = req.query.code;

// TODO: FIX ME - Session no persisting
if (state && state !== req.session.ouathState) {
console.log(req.session.ouathState);
console.log('State did not match original state');
return res.status(400).send();
}

const accessToken = await thirdParty.getAccessToken(code);
req.session.accessToken = accessToken;

res.redirect('http://localhost:3000/');
});

module.exports = router;
```

Please let me know if there is any additional information I can provide to help debug this issue.

Most helpful comment

Hi,

this would be useful documentation to achieve what you want
https://auth0.com/docs/protocols/oauth2/redirect-users

Basically the problem is that the session registered when your user hits the route is different from the one created when you return from the OAuth callback.

So you need to save the redirect url and other info you want to persist in your session DB, use the key generated as a Nonce to be sent as the state parameter in the Oauth request.

Then on Oauth callback you need to get this state query params from the incoming requests, compare. Try to fetch it in the session DB (if not present, don't authorize the auth).

Then restore the info you wanted.

Hope it helped !

All 12 comments

Does anyone look into these issues anymore?

Change your res.redirect(url) to

res.session.save((err)=>{
  if(err){
    console.log(err) 
  }
  res.redirect(url);
})

Sessions are saved lazily to your store, so unless you wait for it to finish saving, a quick redirect may beat it.

See https://github.com/expressjs/session#sessionsavecallback for more info.

BTW, not the maintainer, just passing by.

@crisward - Unfortunately, this still hasn't done the trick. I've even tried to reload the session upon redirect back to my app. Here is the updated code:

router.get('/oauth/login', (req, res) => {
  const ouathState = uuid();
  req.session.ouathState = ouathState;

  const starlingConfig = config.get('starling');
  const clientId = starlingConfig.clientId;
  const redirectUrl = starlingConfig.redirectUrl;

  const starlingOauthUrl = starlingConfig.oauthUrl;

  const url = `${starlingOauthUrl}client_id=${clientId}&response_type=code&state=${ouathState}&redirect_uri=${redirectUrl}`;

  req.session.save(err => {
    if (err) log.error(err);

    res.redirect(url);
  });
});

router.get('/oauth/redirect', (req, res) => {
  const state = req.query.state;
  const code = req.query.code;

  req.session.reload(async err => {
    if (err) log.error(err);

    console.log(req.session);

    // TODO: FIX ME - Session not persisting
    if (state && state !== req.session.ouathState) {
      return res.status(400).send();
    }

    const accessToken = await starling.getAccessToken(code);
    req.session.accessToken = accessToken.data.access_token;
    req.session.refreshToken = accessToken.data.refresh_token;

    res.redirect('http://localhost:3000/');
  });
});

I appreciate you giving it a try though. 馃憤

That's ok, the other thing I'd check is if the session cookie is being set in the browser. Also if you're using a database store, checking there to make sure the session is being saved. If you're going straight to your /oauth/login route and redirecting to another domain the browser may not have chance to write the cookie. Good luck finding the issue.

ping @n6rayan - were you able to find the solution for this?

@gireeshpunathil - Nope, unfortunately not. Sorry I couldn鈥檛 be more help.

I am also facing the same issue.
Do post if anyone gets anything on the same.

Hi,

this would be useful documentation to achieve what you want
https://auth0.com/docs/protocols/oauth2/redirect-users

Basically the problem is that the session registered when your user hits the route is different from the one created when you return from the OAuth callback.

So you need to save the redirect url and other info you want to persist in your session DB, use the key generated as a Nonce to be sent as the state parameter in the Oauth request.

Then on Oauth callback you need to get this state query params from the incoming requests, compare. Try to fetch it in the session DB (if not present, don't authorize the auth).

Then restore the info you wanted.

Hope it helped !

@deveshMantra @gireeshpunathil - I tested out @Yacine-A's theory and he is correct (which actually makes perfect sense). Basically, instead of using Ngrok, I added an entry into my /etc/hosts file that points to my local app and used that as the redirect URL in the OAuth transaction.

This, in conjunction with @crisward's suggestion of saving the session prior to going off to the third-party and reloading the session upon being redirected back to my app, worked a treat.

Hi,

this would be useful documentation to achieve what you want
https://auth0.com/docs/protocols/oauth2/redirect-users

Basically the problem is that the session registered when your user hits the route is different from the one created when you return from the OAuth callback.

So you need to save the redirect url and other info you want to persist in your session DB, use the key generated as a Nonce to be sent as the state parameter in the Oauth request.

Then on Oauth callback you need to get this state query params from the incoming requests, compare. Try to fetch it in the session DB (if not present, don't authorize the auth).

Then restore the info you wanted.

Hope it helped !

@Yacine-A This is the perfect solution. I will save the session with the state as the KEY on my REDIS (session store), before redirecting.Then once the server returns to the callbak, I will use the state on my REDIS as the KEY to retrieve the VALUE with store.get(sid).
When I finish I will post the example

Then once the server returns to the callbak, I will use the state on my REDIS as the KEY to retrieve the VALUE with store.get(sid).

Hey, do you managed to make it work? @AllanOricil

Then once the server returns to the callbak, I will use the state on my REDIS as the KEY to retrieve the VALUE with store.get(sid).

Hey, do you managed to make it work? @AllanOricil

I have the same question, @tambu22, albeit a little different. Here鈥檚 my implementation, I鈥檓 using a library I鈥檓 working on for authentication, and part of the code sets up the session ID to be sent as the state parameter:

params.response_type = 'code';
params.redirect_uri = callbackURL;
params.state = req.session.id;

And upon redirect, on the route, I have this:

router.get('/discordcb', (req, res, next) => {
  let sessionID = req.query.state;
  store.get(sessionID, function storeGetCallback(err, session) {
    if (err) {
      return console.error(err);
    }
    next();
  });
}

So the state parameter calls the session from the store before next() is called, which continues the process of exchanging the auth code for an access token. And so far this works.

My question, and maybe someone can help me out with this, is once store.get(sid, cb) is called, does this load the requested session? That鈥檚 my next step in understanding how this works. Any help is appreciated.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

antishok picture antishok  路  27Comments

scaryguy picture scaryguy  路  16Comments

renehauck picture renehauck  路  16Comments

Matthew-Christopher picture Matthew-Christopher  路  27Comments

azfar picture azfar  路  14Comments