Random logouts when Node.js app goes behind NGINX reverse proxy

I have a next.js app running on a server, (currently not running the node app in docker as doing this broke the expected behaviour of role based auth, separate topic for that issue). It is running on the bare metal at port 3001.

I also have a keycloak auth server running inside a docker container on the same machine at port 8080.

the app is behind the reverse proxy at (fake hostname given here)

subdomain.domain.io

and keycloak is running at

subdomain.domain.io/authorisation

when I run my node app on my local machine and configure the keycloak.json to use the subdomain.domain.io/authorisation auth server it all works as expected.

When I deploy the node app on the remote machine (inside or outside docker) behind the reverse proxy, it randomly logs me out on certain page changes as if the session / cookies are getting lost behind nginx.

I have been stuck on this for weeks… There is nothing of note logged by either keycloak, nginx or the app itself when the logout occurs.

Here is the configuration of keycloak in the docker container

keycloak:
    # restart: always
    container_name: "keycloak-server"
    image: keycloak/keycloak:latest
    ports:
      - "8080:8080"
      - "8443:8443"
    environment:
      TZ: "Europe/London"  
      #~~~~~~~~~~~~~~~~~# User Settings
      KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}    
      KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-password}
      KC_LOG_LEVEL: info
      KC_HEALTH_ENABLED: true
      #~~~~~~~~~~~~~~~~~#
      KC_HOSTNAME: "https://subdomain.domain.io/authorisation" 
      KC_HOSTNAME_ADMIN: "https://admin.subdomain.domain.io/authorisation"
      KC_HOSTNAME_BACKCHANNEL_DYNAMIC: false # needed to allow other containers to commumicate with keycloak server-side
      KC_HTTP_ENABLED: true ##  assume this is edge server, used between nginx and kc
      KC_HOSTNAME_DEBUG: true 
      #~~~~~~~~~~~~~~~~~#
      KC_PROXY_HEADERS: xforwarded ##  enables parsing of non-standard X-Forwarded-* headers, such as X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-Port`
      KC_DB: postgres
      KC_DB_USERNAME: postgres
      KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-password}
      KC_DB_POOL_MAX_SIZE: 50
      KC_DB_URL_HOST: postgres
    command: start --import-realm # --optimized can be added if build has already happened
    volumes:
      - ./realm:/opt/keycloak/data/import:ro # the realm to import
    depends_on:
      - postgres
    networks:
      - auth-network

The client settings in the admin console have standard flow and direct access grants enabled only. I believe all the access settings are correctly allowing the right origin and redirect URLs.

Here is the server.js code, where I think the problem might be, I followed the keycloak docs for nodejs in writing this


app.prepare().then(() => {
  const server = express();

  if (keycloakEnabled) { // do keycloak auth stuff if env var is set
    console.log('the following pages require keycloak authentication', process.env.PROTECTED_PAGES ? colourYellow : colourRed, process.env.PROTECTED_PAGES, colourReset)
    console.log('the following pages require the', process.env.ROLE ? colourYellow : colourRed, process.env.ROLE, colourReset, 'role: ', process.env.ROLE_PROTECTED_PAGES ? colourYellow : colourRed, process.env.ROLE_PROTECTED_PAGES, colourReset)

    server.set('trust proxy', true); // the client’s IP address is understood as the left-most entry in the X-Forwarded-For header.

    if (!dev) {
      let redisClient;
      console.log(`development mode is:`, colourGreen, dev, colourReset, `-> connecting to redis session store at`, colourGreen, `${redisHost}:${redisPort}`, colourReset);
      try {
        redisClient = createClient({
          socket: {
            host: redisHost,
            port: redisPort
          }
        });
      } catch (error) {
        console.log('Error while creating Redis Client, please ensure that Redis is running and the host is specified as an environment variable if this viz app is in a Docker container');
        console.error(error);
      }
      redisClient.connect().catch('Error while creating Redis Client, please ensure that Redis is running and the host is specified as an environment variable if this viz app is in a Docker container', console.error);
      store = new RedisStore({
        client: redisClient,
        prefix: "redis",
        ttl: undefined,
      });
    } else {
      store = new MemoryStore(); // use in-memory store for session data in dev mode
      console.log(`development mode is:`, dev ? colourYellow : colourRed, dev, colourReset, `-> using in-memory session store (express-session MemoryStore())`);
    }

    server.use(
      session({
        secret: 'login',
        resave: false,
        saveUninitialized: true,
        store: store,
        // cookie: {
        //   secure: !dev, // set to true if using https
        //   maxAge: 24 * 60 * 60 * 1000, // 1 day
        //   sameSite: 'lax', // adjust as needed
        //   domain: 'bnl.theworldavatar.io' // ensure this matches your domain
        // }
      })
    );

    const keycloak = new Keycloak({ store: store });
    server.use(keycloak.middleware());

    server.get('/api/userinfo', keycloak.protect(), (req, res) => {
      const { preferred_username: userName, given_name: firstName, family_name: lastName, name: fullName, realm_access: { roles }, resource_access: clientRoles } = req.kauth.grant.access_token.content;
      res.json({ userName, firstName, lastName, fullName, roles, clientRoles });
    });

    server.get('/logout', (req, res) => {
      req.logout(); // Keycloak adapter logout
      req.session.destroy(() => { // This destroys the session
        res.clearCookie('connect.sid', { path: '/' }); // Clear the session cookie
      });
    });

    const protectedPages = process.env.PROTECTED_PAGES.split(',');
    protectedPages.forEach(page => {
      server.get(page, keycloak.protect());
    });
    const roleProtectedPages = process.env.ROLE_PROTECTED_PAGES.split(',');
    roleProtectedPages.forEach(page => {
      server.get(page, keycloak.protect(process.env.ROLE));
      console.log('protecting page', page, 'with role', process.env.ROLE);
    });
  }

and keycloak.json in the node project folder

{
  "realm": "REALM",
  "auth-server-url": "https://subdomain.domain.io/authorisation",
  "ssl-required": "external",
  "resource": "job-portal",
  "public-client": true,
  "use-resource-role-mappings": true,
  "confidential-port": 0
}

If the problem is not there then I suppose it could also be the nginx config (this is config for the subdomain

###########################################################################
## --------------------------------------------------------------------- ##
## --------------------------------- BNL ------------------------------- ##
## --------------------------------------------------------------------- ##
###########################################################################

location / {
        proxy_pass                  http://192.168.1.xxx:3001/;
        proxy_set_header            Host $http_host;
        proxy_set_header            X-Real-IP $remote_addr;
        proxy_set_header            X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header            X-Forwarded-Host $http_host;
        proxy_set_header            X-Forwarded-Server $http_host;
        proxy_set_header            X-Forwarded-Proto https;
    }


########################### keycloak

location /authorisation/realms/ {
    proxy_pass          http://192.168.1.xxx:8080/realms/;
    proxy_set_header    X-Real-IP $remote_addr;
    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 $scheme;
}

location /authorisation/resources/ {
    proxy_pass          http://192.168.1.xxx:8080/resources/;
    proxy_set_header    X-Real-IP $remote_addr;
    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 $scheme;
}

location /authorisation/robots.txt {
    proxy_pass          http://192.168.1.xxx:8080/robots.txt/;
    proxy_set_header    X-Real-IP $remote_addr;
    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 $scheme;
}

Sorry for the long post, but I am completely at a loss as to why my users keep getting logged out. I’ve been stuck on this for weeks and would be so grateful for any help!