Случайные выходы из системы при работе приложения Node.js за обратным прокси NGINX

Вопрос или проблема

Случайные выходы из системы при работе приложения Node.js за обратным прокси NGINX

У меня есть приложение next.js, работающее на сервере (в данный момент я не запускаю приложение node в docker, так как это нарушает ожидаемое поведение аутентификации на основе ролей, это отдельная тема для такой проблемы). Оно работает на “голом” сервере на порту 3001.

У меня также имеется сервер аутентификации keycloak, работающий внутри контейнера docker на том же компьютере на порту 8080.

Приложение находится за обратным прокси на (фейковое имя хоста здесь)

subdomain.domain.io

А keycloak работает на

subdomain.domain.io/authorisation

Когда я запускаю моё приложение node на локальном компьютере по адресу localhost:3001 и настраиваю keycloak.json для использования сервера аутентификации subdomain.domain.io/authorisation, всё работает, как и ожидалось.

Когда я разворачиваю приложение node на удаленном компьютере (внутри или снаружи docker) за обратным прокси, меня случайным образом выкидывает из системы при определенных переходах по страницам, как будто сессия / куки теряются за nginx.

На протяжении нескольких недель я застрял с этой проблемой… В журналах ключа, nginx или самого приложения нет никаких примечательных записей, когда происходит выход из системы.

Вот конфигурация keycloak в контейнере docker

keycloak:
    # restart: always
    container_name: "keycloak-server"
    image: keycloak/keycloak:latest
    ports:
      - "8080:8080"
      - "8443:8443"
    environment:
      TZ: "Europe/London"  
      #~~~~~~~~~~~~~~~~~# Настройки пользователя
      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 # необходимо для связи других контейнеров с ключом на стороне сервера
      KC_HTTP_ENABLED: true ## предполагается, что это крайний сервер, используемый между nginx и kc
      KC_HOSTNAME_DEBUG: true 
      #~~~~~~~~~~~~~~~~~#
      KC_PROXY_HEADERS: xforwarded ## включает парсинг нестандартных заголовков X-Forwarded-*, таких как X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host и 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, если сборка уже произошла
    volumes:
      - ./realm:/opt/keycloak/data/import:ro # область для импорта
    depends_on:
      - postgres
    networks:
      - auth-network

Настройки клиента в административной консоли имеют включенные только стандартный поток и прямые доступные гранты. Я думаю, что все настройки доступа правильно позволяют нужный источник и URL-адреса перенаправления.

Вот код server.js, где я думаю, может быть проблема, я следовал документации keycloak для nodejs при написании этого кода

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

  if (keycloakEnabled) { // выполнять аутентификацию keycloak, если переменная окружения установлена
    console.log('следующие страницы требуют аутентификации keycloak', process.env.PROTECTED_PAGES ? colourYellow : colourRed, process.env.PROTECTED_PAGES, colourReset)
    console.log('следующие страницы требуют роль', process.env.ROLE ? colourYellow : colourRed, process.env.ROLE, colourReset, 'роль: ', process.env.ROLE_PROTECTED_PAGES ? colourYellow : colourRed, process.env.ROLE_PROTECTED_PAGES, colourReset)

    server.set('trust proxy', true); // IP-адрес клиента понимается как самый левый элемент в заголовке X-Forwarded-For.

    if (!dev) {
      let redisClient;
      console.log(`режим разработки:`, colourGreen, dev, colourReset, `-> подключение к redis session store на`, colourGreen, `${redisHost}:${redisPort}`, colourReset);
      try {
        redisClient = createClient({
          socket: {
            host: redisHost,
            port: redisPort
          }
        });
      } catch (error) {
        console.log('Ошибка при создании клиента Redis, убедитесь, что Redis работает и хост указан как переменная окружения, если это визуальное приложение находится в контейнере Docker');
        console.error(error);
      }
      redisClient.connect().catch('Ошибка при создании клиента Redis, убедитесь, что Redis работает и хост указан как переменная окружения, если это визуальное приложение находится в контейнере Docker', console.error);
      store = new RedisStore({
        client: redisClient,
        prefix: "redis",
        ttl: undefined,
      });
    } else {
      store = new MemoryStore(); // использовать хранилище в памяти для данных сессии в режиме разработки
      console.log(`режим разработки:`, dev ? colourYellow : colourRed, dev, colourReset, `-> использование хранилища сессии в памяти (express-session MemoryStore())`);
    }

    server.use(
      session({
        secret: 'login',
        resave: false,
        saveUninitialized: true,
        store: store,
        // cookie: {
        //   secure: !dev, // установить в true, если используется https
        //   maxAge: 24 * 60 * 60 * 1000, // 1 день
        //   sameSite: 'lax', // настраивайте по мере необходимости
        //   domain: 'bnl.theworldavatar.io' // убедитесь, что это соответствует вашему домену
        // }
      })
    );

    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
      req.session.destroy(() => { // Это разрушает сессию
        res.clearCookie('connect.sid', { path: "https://stackoverflow.com/" }); // Очистить куку сессии
      });
    });

    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('защита страницы', page, 'с ролью', process.env.ROLE);
    });
  }

и keycloak.json в папке проекта node

{
  "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
}

Если проблема не в этом, то, возможно, проблема может быть в конфигурации nginx (это конфигурация для подпроекта

###########################################################################
## --------------------------------------------------------------------- ##
## --------------------------------- APP ------------------------------- ##
## --------------------------------------------------------------------- ##
###########################################################################

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;
}

Извините за длинный пост, но я совершенно не понимаю, почему мои пользователи продолжают выходить из системы. Я застрял с этой проблемой на протяжении нескольких недель и был бы очень благодарен за любую помощь!

Ответ или решение

Судя по вашему описанию, проблема с неожиданными выходами из системы в вашем приложении на Node.js, работающем за обратным прокси NGINX, может возникать по нескольким причинам. Разберем основные аспекты, которые могут вызывать эту проблему, и предложим решения.

Возможные причины и решения:

  1. Настройки куки иSameSite атрибуты:
    Убедитесь, что куки для сессий правильно настроены. При работе с обратным прокси важно установить параметры для куки SameSite и secure.

    Вы можете протестировать настройки, раскомментировав блок cookie в вашем коде:

    cookie: {
       secure: true, // Убедитесь, что это true, если у вас включен HTTPS
       maxAge: 24 * 60 * 60 * 1000, // 1 день
       sameSite: 'None', // или 'Lax', в зависимости от вашей конфигурации
       domain: 'subdomain.domain.io' // Убедитесь, что это соответствует вашему домену
    }

    Установка sameSite: 'None' может помочь, если вы находитесь за обратным прокси.

  2. Прокси-заголовки:
    Убедитесь, что в NGINX правильно настроены заголовки X-Forwarded:

    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $http_host;

    Убедитесь, что ваши настройки соответствуют тому, как Keycloak ожидает обращаться к вашему приложению.

  3. Перепроверьте настройки Keycloak:
    В keycloak.json убедитесь, что URL для аутентификации и конфигурации клиента совпадают с тем, как они настроены в NGINX:

    "auth-server-url": "https://subdomain.domain.io/authorisation",

    Также убедитесь, что перенаправления из клиентских приложений настроены правильно в консоли управления Keycloak, и они не содержат лишних символов или пробелов.

  4. Проверка настроек NGINX:
    Ваша конфигурация NGINX выглядит в целом корректной, но могло бы помочь следующее:

    • Настройте кэширование, чтобы NGINX не кэшировал запросы сессий или пользователями. Это может привести к тому, что NGINX будет кэшировать состояние аутентификации.
    • Включите журналирование, чтобы понять, какие запросы приходят на сервер, и возможно, какие заголовки приходят и уходят.
  5. Валидация сеансов:
    Здесь стоит дополнительно проверить, как хранятся сессии. Используйте Redis для более стабильного хранения сессий (как указано в вашем коде). Убедитесь, что Redis работает должным образом и соединение установлено без ошибок.

  6. Логи и отладка:
    Добавьте больше инструментов для логирования и отладки в ваше приложение. Это поможет вам получить больше информации о том, что происходит перед тем, как сессия завершается. Вы можете попробовать использовать библиотеки, такие как morgan или winston, для получения подробных логов.

Подводя итог:

Проблема с неожиданными выходами из системы в вашем Node.js приложении за NGINX может быть связана с настройками куки, параметрами прокси и обработкой сессий. Проверка каждого из этих аспектов и внесение необходимых правок может помочь решить проблему. Следует также учитывать возможность использования инструментов отладки для улучшения видимости и анализа происходящих процессов.

Если ни одно из предложенных решений не помогает, может быть целесообразным создать минимальный пример вашего приложения и конфигурации, который воспроизводит проблему, для дальнейшего изучения и диагностики.

Оцените материал
Добавить комментарий

Капча загружается...