Express + Passport + Nginx - req.user empty only on live environment

Viewed 224

I have an application running that fetches user data from my express API using Passport for Google OAuth2 identification. On live environment request.user comes back empty, on dev it is working.

router.get('/user', (request, response) => {
  response.send(request.user);
});

I've deployed the front-end on Netlify and the express API on a nginx server from digital ocean.

I think it's something with the express session? I've tried many thing already:

Setting the server options as follows:

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.set('trust proxy', 1);
app.use(function (request, response, next) {
  response.header('Access-Control-Allow-Credentials', true);
  response.header(
    'Access-Control-Allow-Origin',
    request.headers.origin,
  );
  response.header(
    'Access-Control-Allow-Methods',
    'GET,PUT,POST,DELETE',
  );
  response.header(
    'Access-Control-Allow-Headers',
    'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept',
  );
  if ('OPTIONS' == request.method) {
    response.send(200);
  } else {
    next();
  }
});
app.use(
  session({
    secret: process.env.COOKIE_SECRET,
    resave: false,
    saveUninitialized: true,
  }),
);
app.use(passport.initialize());
app.use(passport.session());

My front-end fetch call contains credentials:

const response = await fetch(`${API_PATH}auth/user`, {
    method: 'GET',
    credentials: 'include',
});

Adding this to the nginx configuration:

location  / {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
}

In my local environment the request headers contain a cookie, on live environment only the Set-Cookie is set in the response.

Please find the full repository here: https://github.com/sanderdebr/flightscanner

1 Answers

Edit

My original thinking was correct. I found your site and was able to confirm Chrome is blocking the cookie.

Set-Cookie: connect.sid=s%....g37cv9X...sbdA; Domain=flightscanner.netlify.app; Path=/; HttpOnly; Secure

This set cookie was blocked because its domain attribute was invalid with regards to the current host url.

This will resolve your issues cookie: {httpOnly: false} but is not the correct way to do it. You will need to figure out what is wrong with your NGINX config and fix that.

Original

Line 5 of your gist shows Path=/; HttpOnly If you hitting you frontend over HTTPS then chrome is going to ignore the Set-Cookie header. This hints at an issue with your nginx config. One thing you can do FOR TESTING ONLY is:

app.use(
  session({
    secret: process.env.COOKIE_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: {httpOnly: false},
  }),
);

You'll need to make sure and clear any site data in Chrome.

Get Fiddler 4 and set it up to proxy SSL/HTTPS traffic. This will help you with getting headers right and seeing other issues. Additionally, in Chrome DEV tools select the entry that has the login request. Check the header tab for the Set-Cookie and then the Cookies tab. My guess is you will see the Set-Cookie with the HttpOnly and the cookie is store but chome wont send it with your fetch request.

To provide some additional help I'm going to add in part of our nginx config from production. I can't post the full config, sorry. Keep in mind we use Docker so some parts wont apply and will need to be changed to match your environment.

Here is our express/passport session configs:

    expressSession: {
        name: EXPRESS_SESSION_NAME,
        secret: EXPRESS_SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        proxy: true,
        rolling: true,
        cookie: {
            maxAge: 15 * MINUTE,
            sameSite: 'none',
            secure: !isDev,
            httpOnly: !isDev,
            domain: EXPRESS_SESSION_COOKIE_DOMAIN,
        }
    },
    passportSession: {
        httpOnly: !isDev,
    }

We run multiple subdomains one of which is our Portal a in house customer React/Express app. The express part is running in a docker container. To handle this we need to handle CORS on a per subdomain bases. We use mapping because IF is truly evil! see: https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/

# setup out mappings in HTTP block outside SERVER blocks
# Used to disabled logging
map $request_uri $loggable {
    /healthcheck 0;
    default 1;
}
# allow all subdomains - each subdomain will get their own server block
map $http_origin $cors_origin {
    default "";
    "~^https://(localhost|.*\.ourdomain.network)$" "$http_origin";
}

# Websockets
map $http_upgrade $connection_upgrade {
    default upgrade;
    ""      close;
}

Basic server block here. We use NGINX to off load SSL/HTTP2. We have an AWS NLB in front but could not uses ALB as we have some uses who auth with an SSL cert. Not shown in this config.

# This is a server block for a subdomain
server {
    listen      443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    server_name     portal.ourdomain.network;
    allow           all;
    access_log      /dev/stdout proxy_log;
    error_log       stderr      debug;
    set             $handler    "https.portal.ourdomain.network";

    # prevent Cross-site scripting (XSS) by enabling built in browser filter
    # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
    add_header X-XSS-Protection "1; mode=block";

    # enable HSTS (HTTP Strict Transport Security) to avoid ssl stripping https://en.wikipedia.org/wiki/SSL_stripping#SSL_stripping
    # https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security
    add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload" always;

    include sites/star.ourdomain.network/portal/*.nginx;
}

This is the part you should pay closest attention to. Here we have a location for handling API request IE the traffic going to our express app. We have NGINX handle the preflight request as it's just easier and less error prone. Some of these headers have more than they should. We will be testing over the next month or so trimming them down to the bare minimum needed.

# API Location Block
location /api {
    set                     $handler "$http_origin/api";
    access_log              /dev/stdout proxy_log;
    error_log               stderr debug;
    resolver                127.0.0.11;         # Docker DNS
    proxy_redirect          off;

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Vary                             Origin;
    add_header Access-Control-Allow-Credentials true;
    # Always allowed Accept, Accept-Language, Content-Language, Content-Type
    add_header Access-Control-Allow-Headers     "Origin,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range";
    add_header Access-Control-Allow-Methods     "OPTIONS,GET,PUT,POST,DELETE";

    if ($request_method = 'OPTIONS') {
        set $handler "OPTIONS:portal.ourdomain.network:443/api";
        ## DOMAIN/SUBDOMAIM CORS Preflight
        add_header 'Access-Control-Allow-Origin'    $cors_origin;
        add_header Vary                             Origin;
        add_header 'Access-Control-Allow-Methods'   'OPTIONS,GET,PUT,POST,DELETE';
        # Custom headers and headers various browsers *should* be OK with but aren't
        add_header 'Access-Control-Allow-Headers'   'Origin,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        # Tell client that this pre-flight info is valid for 20 days
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        add_header Sec-Fetch-Site same-site;
        return 204;
    }

    proxy_set_header        Host                  $host;
    proxy_set_header        X-Real-IP             $remote_addr;
    proxy_set_header        X-Scheme              $scheme;
    proxy_set_header        X-Forwarded-For       $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Host      $server_name;
    proxy_set_header        X-Forwarded-Proto     https;
    proxy_set_header        Upgrade               $http_upgrade;
    proxy_set_header        Connection            $connection_upgrade;
    proxy_connect_timeout   60;
    proxy_read_timeout      60;

    # We have to do it this way otherwise NGINX cannot resolve the container and will fail to start
    set $proxy_host         "server:4000";
    proxy_pass              http://$proxy_host;
}

This is an include file for the server block to set the front end version. We looking at making this header based with a default.

set $s3Bucket       "portal.ourdomain.network.s3-website-us-gov-west-1.amazonaws.com";
set $version        "0.1.28";

This is just a basic location to proxy the front end traffic to an AWS S3 bucket.

# Front End Location
location / {
    set $handler            "$http_origin/";
    access_log              /dev/stdout proxy_log;
    error_log               stderr debug;
    ssi on;
    resolver                8.8.8.8;
    proxy_intercept_errors  on;
    proxy_redirect          off;
    proxy_set_header        Host $s3Bucket;
    proxy_hide_header       x-amz-id-2;
    proxy_hide_header       x-amz-request-id;

    add_header Access-Control-Allow-Origin      $cors_origin;
    add_header Vary                             Origin;
    add_header Access-Control-Allow-Headers     *;
    add_header Access-Control-Allow-Methods     'GET, OPTIONS';
    proxy_set_header        X-Real-IP             $remote_addr;
    proxy_set_header        X-Scheme              $scheme;
    proxy_set_header        X-Forwarded-For       $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto     https;
    proxy_connect_timeout   60;
    proxy_read_timeout      60;

    rewrite                 ^/(.*)$ /$version/$1 break;
    proxy_pass              http://$s3Bucket;
}
Related