Search code examples
node.jsfirebaseexpressgoogle-cloud-functionsexpress-session

Unable to Retrieve Session Cookie of Express-Session and Connect-Session-Firebase


I am trying to complete an oauth-1 handshake. My backend is hosted on Google Functions. I'll walk through the flow quickly for context:

  1. User clicks in frontend to add Etsy connection to my app. My frontend then contacts an appropriate endpoint in my backend to initiate this process.
  2. My backend requests a token from Etsy, and I receive a token, a secret, and a url to redirect the users to so that they can sign in. I try to store the token and secret in a session cookie for later -- since I am using Google Functions, the backend doesn't "persist" per se and so I am trying to store these values to be retrieved on next execution/stage. (Note: In requesting the token, I provide a callback URL which will be contacted on user sign in.)
  3. My frontend receives the url from step 2 and redirects the user there to login.
  4. The user approves via that login, and the callback URL is called. This includes a verifier. I must then get an accessToken using the verifier and the token and secret from step 2. Although I thought these would be retrieved in the session, they are both undefined, and so my request for an accessToken fails.

It seems to me like at every invocation a new session is being made, regardless of whether there already is a session in existence. If I examine the session at step 1 and step 2, the timestamps on them are different, making me think that rather than retrieving the session from step 1, step 2 is making a new one -- which could very well be because of how I am doing app.use(session).

List of the technologies I am using:

  • node.js
  • express
  • express-session
  • firebase-functions
  • connect-session-firebase (Have also tried firestore-store, with very slight adjustments to the code below to match its basic setup.)

Here is my backend implementation for this:

const url = require('url');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require("express");
const app = express();
const cors = require("cors")
const session = require('express-session')
const FirebaseStore = require('connect-session-firebase')(session);

// Etsy OAuth
// For Etsy OAuth
const etsyjs = require('etsy-js');
const client = etsyjs.client({
  key: process.env.ETSY_KEY,
  secret: process.env.ESTY_SECRET,
  callbackURL: '<location>/authorize'
});

const ref = admin.initializeApp({
  credential: admin.credential.cert(json), //json retrieved and put here
  databaseURL: 'https://<app>.firebaseio.com'
});

app.use(cors())
app.use(session({
    store: new FirebaseStore({
         database: ref.database(),
    }),
    secret: 'My secret',
    resave: true,
    saveUninitialized: true,
    name: '__session',
    cookie: {
      maxAge : 60000,
        secure: false,
        httpOnly: false 
     }
    }
));

// Step 1 for backend of OAuth
app.get('/begin-oauth', (req, res) => {
  res.setHeader('Cache-Control', 'private'); // I read somewhere this needs to be set, but I am not seeing a difference either way with it.

  return client.requestToken((err, response) => {
    if (err) {
      return console.log(err);
    }

    req.session.token = response.token;
    req.session.sec = response.tokenSecret;

    res.status('200').send(response) // returning url to send the user there via frontend.
  });
});

// Step 2 for backend of OAuth
app.get('/authorize', (req, res) => {
  
  let query, verifier;
  query = url.parse(req.url, true).query;
  verifier = query.oauth_verifier;

  // session at this point should have token and sec stored from before but are not present.
  return client.accessToken(req.session.token, req.session.sec, verifier, (err, response) => {
    if (err) {
      console.log(err);
    }
    req.session.token = response.token;
    req.session.sec = response.tokenSecret;

    // Finish up.
  });
});

Solution

  • Okay this is now all working. There were a few things that made it work.

    1. Including credentials in the backend call so that now looks like this:
    return await fetch(url, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'include',
      }).then(...)
    
    1. Adding credentials to the cors settings server-side. So that now looks like this:
    var corsOptions = {
      credentials: true,
      origin: true
    }
    
    app.use(cors(corsOptions))
    
    1. These still hadn't fully fixed the issue. What did ultimately solve the problem is that I cleared my cache from my browser. I believe this is the thing that really solved the issue! Definitely painful how long it took to try this simple thing.

    UPDATE:

    So the above fixed everything in a development environment, but not when I actually deployed to Google Functions. At that point I hit a series of new issues around cors. Here is the error messages I received:

    enter image description here

    And here is what I changed to fix this:

    app.use(function(req, res, next) {
      var allowedOrigins = ['http://localhost:3000', 'http://localhost:5000', 'https://www.<site>.com', 'https://<app-location>.cloudfunctions.net', 'https://<app>.firebaseio.com']
      var origin = req.headers.origin;
      functions.logger.log('Checking headers:')
      functions.logger.log(origin)
      functions.logger.log(req.headers)
      if(allowedOrigins.indexOf(origin) > -1){
           res.setHeader('Access-Control-Allow-Origin', origin);
      }
    
      res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
      res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
      res.header('Access-Control-Allow-Credentials', true);
      return next();
    });
    

    and I updated the cookie to be:

    cookie: {
      maxAge: 30*24*60*60*1000, 
      secure: true, 
      httpOnly: false, 
      sameSite: 'none'
    }
    

    ^ in particular, I set secure: true and sameSite: 'none'

    UPDATE 2:

    With the above I got it working in a deployed form, but it wasn't working in a Chrome Incognito window, which was making me nervous that perhaps it was only working because I had something cached still that I wasn't tracking down. Turns out that it is a security feature of incognito windows. You'll have to toggle this OFF for the session to work properly:

    enter image description here