
Подключить MFA к современному веб-приложению обычно несложно: достаточно подключить SAML или OIDC на стороне самого приложения и включить второй фактор на Identity Provider. Проблемы начинаются там, где сервис не умеет ни в SAML, ни в OIDC, а переписывать его рискованно, дорого или попросту некому.
Во многих корпоративных сетях до сих пор живут монолитные legacy-системы, которые лучше не трогать, и кастомные сервисы, давно оставшиеся без активного развития. На такой случай придумана концепция предаутентификации. Она позволяет вынести всю сложную логику проверки прав, работу с токенами и криптографией на внешний контур. По сути, перед приложением устанавливается барьер, который отсекает нелегитимные запросы еще до того, как они дойдут до бэкенда.
В этой статье системный инженер Артур Газеев и я, Аскар Добряков, ведущий эксперт направления защиты данных и приложений в К2 Кибербезопасность разбираем, как вынести MFA на периметр для legacy-системы, которую нельзя быстро переписать. Покажем архитектуру решения, объясним, почему выбрали связку Nginx + OAuth2 Proxy + Indeed AM, и разберем, на каких настройках поднимали и отлаживали эту схему.
Что такое предаутентификация, и чем она отличается от нормальной
Суть предаутентификации в том, чтобы вынести логику проверки доступа за пределы приложения и добавить еще один этап аутентификации на уровне реверс-прокси. Если клиент не прошел проверку на периметре, его трафик вообще не должен дойти до защищаемого приложения. Для этого нам потребуются Nginx, прокси-менеджер и провайдер идентификации.
Теоретически эту задачу можно решить сторонним OIDC-модулем для Nginx, но тогда пришлось бы завязываться на нестандартную сборку веб-сервера и брать на себя дополнительные риски сопровождения и атак цепочки поставок. Поэтому, разрабатывая эту схему, мы сознательно оставили Nginx в стандартной сборке, а логику аутентификации вынесли в отдельный компонент — OAuth2 Proxy. Мы смотрели и на альтернативы вроде Gatekeeper, но в нашем сценарии они оказались менее гибкими по интеграции и маршрутизации.
Выбор Identity Provider — отдельная тема. Сперва мы рассматривали Keycloak. У него зрелая поддержка OIDC и SAML, гибкая настройка MFA, большая экосистема и понятная модель кастомизации. Но в нашем случае важны не только функции, но и эксплуатация: SLA, формальная зона ответственности, предсказуемые обновления.
В итоге в этом проекте мы остановились на Indeed AM. Решающими оказались три фактора: готовая встраиваемость во внутренний корпоративный контур, единая точка управления политиками доступа и вендорская поддержка.
Остается Nginx. Сам по себе он не умеет в SSO и ничего не знает об OIDC, зато у него есть штатный модуль auth_request. Он позволяет приостановить основной запрос, выполнить внутренний подзапрос к внешнему сервису и либо пустить клиента дальше, либо отправить его на аутентификацию.
В нашей схеме это работает так:
Клиент стучится на защищаемый URL, а Nginx приостанавливает обработку и отправляет копию заголовков запроса в сторону OAuth2 Proxy.
OAuth2 Proxy изучает данные исходного запроса в поисках куки авторизации, подтверждающей, что у пользователя есть активная сессия.
Сценарий А: если валидная кука найдена, прокси отвечает кодом 200 OK или любым другим статусом из диапазона 2xx. Для Nginx это сигнал, что запрос можно пропустить дальше на бэкенд.
Сценарий Б: если куки нет или ее срок действия истек, OAuth2 Proxy возвращает 401 Unauthorized. Nginx перехватывает этот ответ и превращает его в 302 Found, отправляя браузер пользователя на страницу авторизации Identity Provider.
Сценарий В: если в ходе проверки выясняется, что доступ пользователю запрещен, например из-за нехватки ролей, сервис возвращает 403 Forbidden, а запрос обрывается.
После перенаправления на страницу IdP пользователь вводит свои учетные данные и проходит второй фактор (например, вводит TOTP-код из приложения). Если все верно, IdP редиректит его обратно на callback-адрес OAuth2 Proxy. Прокси выписывает сессионную куку, и цикл повторяется, но теперь подзапрос Nginx возвращает 200 OK.

На практике это означает простую вещь: legacy-приложение остается нетронутым, а контроль доступа переносится на внешний контур. Бэкенд получает уже проверенный запрос с данными о пользователе в заголовках, а управление сессией, редиректами и MFA берут на себя Nginx, OAuth2 Proxy и IdP.
Для корпоративного контура у такой схемы есть еще один плюс: она создает удобную точку наблюдения. Поскольку любой неавторизованный запрос к защищаемому приложению неминуемо заканчивается перенаправлением на Identity Provider, он становится удобной точкой для мониторинга при помощи SIEM.
Шаг 1. Установка и настройка OAuth2 Proxy
Для начала перейдем на Linux-сервер и установим актуальную версию OAuth2 Proxy.
Свежие релизы доступны на GitHub:
wget https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.5.1/oauth2-proxy-v7.5.1.linux-amd64.tar.gz tar xvzf oauth2-proxy-v7.5.1.linux-amd64.tar.gz mv oauth2-proxy-v7.5.1.linux-amd64 /opt
Для удобства работы делаем символьную ссылку:
ln -s /opt/oauth2-proxy-v7.5.1.linux-amd64 /opt/oauth2-proxy
Затем создаем сервисного пользователя, группу и выставляем права:
export PATH="$PATH:/sbin:/usr/sbin:usr/local/sbin" useradd -d /dev/null -s /usr/sbin/nologin oauth2-proxy groupadd oauth2-proxy usermod -a -G oauth2-proxy oauth2-proxy chmod -R 775 /opt/oauth2-proxy chown -R oauth2-proxy:oauth2-proxy /opt/oauth2-proxy/ chmod 755 /etc/oauth2-proxy.cfg
Вся логика работы этого прокси хранится в файле /etc/oauth2-proxy.cfg.
Ниже — стендовый конфиг с комментариями. В нем намеренно оставлены небезопасные параметры, которые использовались только для отладки: отключенный PKCE, cookie_secure="false", cookie_httponly="false", пропуск проверки сертификатов и отладочные заголовки с данными сессии.
Конфиг с комментариями:
# === Основные параметры === skip_provider_button = "true" # Пропускать кнопку выбора провайдера на странице входа. # Полезно, если только один провайдер OAuth2. reverse_proxy="true" # Режим обратного прокси. # Включает поддержку заголовков X-Forwarded-*, без которых прокси не поймет, с какого реального IP и по какому протоколу пришел клиент.Это сломает механизм формирования Callback URL. provider="oidc" # Тип провайдера -- OpenID Connect (OIDC), современный протокол поверх OAuth2. provider_display_name="Indeed IdP" # Отображаемое имя провайдера на странице входа. skip_oidc_discovery="true" # Пропускаем автоматическое обнаружение endpoints через .well-known/openid-configuration. # Все endpoints заданы вручную ниже. code_challenge_method="" # Метод PKCE (Proof Key for Code Exchange) -- отключен. # Обычно "S256" для усиления безопасности. errors_to_info_log="true" # Записывать ошибки в info-лог в дополнение к error-логу. https_address=":4180" # Адрес и порт, на котором запускается oauth2-proxy. # === Учетные данные клиента === client_id="nginx_oauth" # Идентификатор клиента -- должен совпадать с ClientId в app-settings.json на стороне IdP. client_secret="my_top_secret" # Секрет клиента -- должен совпадать с ClientSecret на стороне IdP. # ВНИМАНИЕ: В продакшене не стоит оставлять секреты в конфиге в открытом виде. Используйте переменные окружения или менеджеры секретов (HashiCorp Vault). # === URL эндпоинтов === redirect_url="https://oauth2.test.ru:4180/oauth2/callback" # Callback URL -- сюда IdP вернет пользователя после успешной аутентификации. login_url="https://idp.test.ru/am/idp/connect/authorize" # URL для начала процесса авторизации OAuth2. backend_logout_url="https://oauth2.test.ru:4180/am/idp/connect/logout" # URL для выхода из системы на стороне провайдера. profile_url="https://idp.test.ru/am/idp/connect/userinfo" # URL для получения информации о пользователе (OIDC userinfo endpoint). redeem_url="https://idp.test.ru/am/idp/connect/token" # URL для обмена authorization code на access token. oidc_jwks_url="https://idp.test.ru/am/idp/.well-known/jwks" # URL для получения JSON Web Key Set (публичные ключи для проверки JWT oidc_issuer_url="https://idp.test.ru/am/idp" # URL издателя (issuer) токенов. # === Заголовки === set_xauthrequest="true" # Устанавливать заголовки X-Auth-Request-* для передачи данных аутентификации. # Когда аутентификация пройдет, прокси вытащит из JWT информацию о пользователе и положит ее в HTTP-заголовки ответа, чтобы Nginx смог их забрать. set_authorization_header="true" # Устанавливать Authorization заголовок с Bearer-токеном. pass_access_token="true" # Передавать access token в бэкенд. pass_authorization_header="true" # Передавать Authorization заголовок в бэкенд. pass_user_headers="true" # Передавать заголовки с информацией о пользователе (X-User, X-Email). pass_host_header="true" # Передавать оригинальный Host заголовок. whitelist_domains=["*.ru", ".test.ru"] # Белый список доменов для перенаправления после аутентификации. # Предотвращает атаки open redirect. insecure_oidc_allow_unverified_email="true" # Разрешить неверифицированные email-адреса. # ВНИМАНИЕ: Потенциальный риск безопасности! scope="openid" # Запрашиваемый scope. openid -- минимальный для OIDC. # Можно добавить: profile, email, offline_access и др. oidc_email_claim="email" # Название claim в ID-токене, содержащего email. email_domains=["*"] # Разрешенные домены email-адресов. ["*"] -- все домены разрешены. # === Cookie === cookie_secret="vNQ9Z1RCjIPvEQC5H0a72JEwP1vEEQ==" # Секрет для шифрования куки (16, 24 или 32 байта для AES-128/192/256). # ВНИМАНИЕ: В продакшене не стоит остав секреты в конфиге в открытом виде. Используйте переменные окружения или менеджеры секретов (HashiCorp Vault). cookie_secure="false" # Передавать куки только по HTTPS. false -- разрешает HTTP (только для стенда!). cookie_httponly="false" # Запретить доступ к куки через JavaScript. false -- позволяет доступ (риск XSS!). cookie_name="_oauth2_cookie" # Имя сессионной куки. cookie_samesite="lax" # SameSite-атрибут для базовой защиты от CSRF. cookie_csrf_per_request="true" # Генерировать новый CSRF-токен для каждого запроса. cookie_csrf_expire="15m0s" # Время жизни CSRF-токена (15 минут). cookie_expire="8h0m0s" # Время жизни сессионной куки (8 часов). cookie_refresh="1h0m0s" # Интервал обновления куки (1 час). cookie_domains=".prod.rus" # Домен, для которого устанавливается куки. # Точка в начале -- куки будет доступна для всех поддоменов. # === SSL/TLS === force_https="true" # Принудительное перенаправление HTTP → HTTPS. ssl_upstream_insecure_skip_verify="true" # Не проверять SSL-сертификаты бэкенд-серверов. # ВНИМАНИЕ: Опасная настройка для продакшена! ssl_insecure_skip_verify="true" # Не проверять SSL-сертификаты OAuth2-провайдера. # ВНИМАНИЕ: Опасная настройка для продакшена! tls_cert_file="/etc/ssl/private/oauth2.test.ru.crt" # Путь к SSL-сертификату для oauth2-proxy. tls_key_file="/etc/ssl/private/oauth2.test.ru.key" # Путь к приватному ключу SSL. tls_min_version="TLS1.2" # Минимальная версия TLS (устаревшие TLS 1.0/1.1 отключены). # === Логирование === logging_filename="/var/log/oauth02/logs.log" # Путь к файлу логов. logging_max_size=10 # Максимальный размер файла лога в мегабайтах перед ротацией. logging_max_backups=5 # Максимальное количество архивных файлов логов. logging_max_age=30 # Максимальный возраст архива логов в днях. logging_compress=true # Сжимать архивные логи. logging_local_time=true # Использовать локальное время вместо UTC.
Создадим systemd-юнит для автозапуска демона:
sudo systemctl edit --full --force oauth2-proxy.service
Содержимое юнита:
[Unit] Description=reverse proxy [Service] Type=simple User=oauth2-proxy WorkingDirectory=/opt/oauth2-proxy EnvironmentFile=/etc/oauth2-proxy.cfg ExecStart=/opt/oauth2-proxy/oauth2-proxy --config=/etc/oauth2-proxy.cfg Restart=always ExecReload=/bin/kill -s HUP $MAINPID KillMode=process [Install] WantedBy=multi-user.target
Активируем и запускаем:
systemctl daemon-reload systemctl enable oauth2-proxy systemctl start oauth2-proxy
Провайдер настроен, прокси запущен и слушает порт 4180.
Шаг 2. Регистрация клиента в Indeed AM
Итак, провайдер настроен, прокси запущен и слушает порт 4180. Теперь научим IdP доверять OAuth2 Proxy. Для этого развернем Indeed AM в инфраструктуре Windows на базе IIS (Internet Information Services). Первым делом идем на сервер провайдера (idp.test.ru) и открываем конфигурационный файл C:\inetpub\wwwroot\am\idp\app-settings.json.
Здесь мы регистрируем нового клиента с идентификатором nginx_oauth. Прописываем общий секретный ключ (ClientSecret), разрешенные Redirect URIs (куда IdP должен вернуть пользователя после успешного ввода пароля и второго фактора) и запрашиваем список Permissions. Без этого блока настроек IdP просто отвергнет любой запрос от нашего прокси как нелегитимный.
{ "ClientId": "nginx_oauth", "ClientSecret": "my_top_secret", "DisplayName": "nginx proxy auth request oauth2", "Permissions": [ "ept:authorization", "ept:device", "ept:introspection", "ept:logout", "ept:revocation", "ept:token", "gt:authorization_code", "gt:client_credentials", "gt:urn:ietf:params:oauth:grant-type:device_code", "gt:implicit", "gt:password", "gt:refresh_token", "rst:code", "rst:code id_token", "rst:code id_token token", "rst:code token", "rst:id_token", "rst:id_token token", "rst:none", "rst:token", "scp:address", "scp:email", "scp:phone", "scp:profile", "scp:roles" ], "RedirectUris": [ "https://oauth2.test.ru:4180/oauth2/callback" ] }
Параметры ClientId и ClientSecret должны совпадать с теми, что мы указали в конфиге OAuth2 Proxy. RedirectUris — это адрес, на который IdP вернет ответ через браузер пользователя после успешной аутентификации. Далее, в том же файле app-settings.json, нужно научить провайдера отправлять email-адрес пользователя в ответе. В секции CustomAttributes добавляем:
{ "ServiceProvider": "nginx_oauth", "Attributes": [ { "Name": "email", "UserNameFormat": "Email" } ] }
Один из важных моментов на стороне IIS — настройка сертификата для работы с протоколом OIDC, который используется для криптографической подписи ответов (JWT-токенов). В секции OIDC → IDP необходимо жестко задать отпечаток сертификата:
"OIDC": { "IDP": { "CertificateThumbprint": "d2846fb4d27f1865418501618dbbe51d9e4aae42" } }
Этот сертификат должен лежать в локальном хранилище системы (Local Computer Personal Store). Сохраняем файл, перезапускаем пул IIS, и провайдер готов к приему гостей.
Шаг 3. Настройка Nginx
Переходим к серверу nginx.test.ru. Обновляем пакеты и ставим Nginx:
apt-get update -y && apt-get upgrade -y && apt-get dist-upgrade -y sudo apt install nginx -y
Подготовим файлы SSL-сертификатов:
mkdir /etc/nginx/ssl
Размещаем в каталоге открытую часть сертификата (proxy.crt) и приватный ключ без пароля (proxy_private.key):
/etc/nginx/ssl/proxy.crt /etc/nginx/ssl/proxy_private.key
Генерируем файл параметров Диффи-Хеллмана (рекомендуемая минимальная длина ключа — 4096 бит). Обычно этот процесс занимает несколько минут: Ключи DH будут помещены в /etc/nginx/ssl/dhparam.pem
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
Создаем файл настроек виртуального хоста:
nano /etc/nginx/sites-available/test.indeed.local
Ниже приведен стендовый конфиг, на котором мы отлаживали механику взаимодействия компонентов. В продакшене часть параметров должна быть ужесточена: включен PKCE, проверка сертификатов, флаги secure/httponly для cookie.
Полный стендовый конфиг с подробным разбором каждого блока.
server { underscores_in_headers off; # Запрещаем нижние подчеркивания в HTTP-заголовках. Старые версии Apache и некоторые ретро-серверы по умолчанию отбрасывают заголовки с подчеркиваниями, считая их невалидными. Если забыть эту строку, можно потерять половину пользовательского контекста. listen 443 ssl; server_name test.indeed.local; ssl_certificate /etc/ssl/private/proxy.crt; ssl_certificate_key /etc/nginx/ssl/proxy_private.key; # === Буферы -- главные грабли проекта === large_client_header_buffers 8 512k; proxy_buffering on; proxy_buffer_size 512k; proxy_buffers 8 512k; proxy_busy_buffers_size 512k; proxy_max_temp_file_size 2048m; proxy_temp_file_write_size 512k; # JWT-токены в корпоративной среде обрастают гигантским количеством claim-ов: списки ролей, идентификаторы групп из Active Directory, права доступа к десяткам подсистем, метаданные об устройстве из Indeed AM. Если размер заголовка Cookie превышает лимит стандартного буфера, Nginx молча обрывает соединение или выплевывает 400 Bad Request. Мы увеличили до 8 буферов по 512 КБ (суммарно 4 МБ на заголовки). access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # === Маршрутизация OAuth2 Proxy === location /oauth2/ { # Все запросы, начинающиеся с /oauth2/ (кроме явно указанных ниже). proxy_pass https://oauth2.test.ru:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Auth-Request-Redirect $scheme://$host:443$request_uri; proxy_set_header X-Forwarded-Port 443; } location = /oauth2/auth { # Точное совпадение, сюда auth_request делает внутренние подзапросы. proxy_pass https://oauth2.test.ru:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Uri $request_uri; proxy_set_header Content-Length ""; proxy_pass_request_body off; # Для проверки аутентификации не нужно гонять тело запроса (POST-данные, файлы). Прокси интересуют только заголовки с куками. proxy_set_header X-Forwarded-Port 443; proxy_set_header X-Auth-Request-Redirect $scheme://$host:443$request_uri; } location /oauth2/sign_out { # Обработка выхода из системы. proxy_pass https://oauth2.test.ru:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Uri $request_uri; proxy_set_header X-Forwarded-Port 443; proxy_set_header X-Auth-Request-Redirect $scheme://$host:443$request_uri; } # === Основной блок -- защищаемое приложение === location / { resolver 10.10.10.10; # DNS-резолвер -- нужен, если в proxy_pass используется доменное имя. set $app_server ""; proxy_pass <имя сервиса>; # Замените на реальный адрес бэкенда. proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Auth-Request-Redirect ; add_header Cache-Control 'no-store, no-cache'; # Запрещаем кэширование. proxy_connect_timeout 1800s; proxy_send_timeout 1800s; proxy_read_timeout 1800s; # ВНИМАНИЕ: Все три таймаута выкручены на 30 минут, это много. Для прода будет достаточно 60-90 секунд. proxy_request_buffering off; # --- Предаутентификация --- auth_request /oauth2/auth; # Включаем проверку: перед доступом к контенту Nginx делает подзапрос. error_page 401 =302 /oauth2/sign_in; # Если подзапрос вернул 401, то ловим ошибку и отдаем 302 на страницу входа. # --- Извлечение данных из ответа auth_request --- auth_request_set $user $upstream_http_x_auth_request_user; auth_request_set $email $upstream_http_x_auth_request_email; auth_request_set $token $upstream_http_x_auth_request_access_token; auth_request_set $auth_cookie $upstream_http_set_cookie; # Сохраняем заголовки из ответа OAuth2 Proxy во внутренние переменные Nginx. add_header Set-Cookie $auth_cookie; # Устанавливаем куки аутентификации клиенту. # --- Обработка множественных кук --- auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1; if ($auth_cookie ~* "(; .*)") { set $auth_cookie_name_0 $auth_cookie; set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1"; } if ($auth_cookie_name_upstream_1) { add_header Set-Cookie $auth_cookie_name_0; add_header Set-Cookie $auth_cookie_name_1; } # --- Передача данных аутентификации бэкенду --- proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-User $user; proxy_set_header X-Email $email; proxy_set_header X-Access-Token $token; # Бэкенд получает чистый HTTP-запрос с заголовком X-User: ivan.ivanov. # Nginx должен перезаписывать эти заголовки. Без proxy_set_header атакующий может подсунуть поддельный заголовок X-User: admin, и легаси-бэкенд послушно отдаст админ-панель. ### DEBUG -- только для стенда! add_header X-Debug-Token $token; add_header X-Debug-User $user; add_header X-Debug-Email $email; # ВНИМАНИЕ: В продакшене эти заголовки необходимо убрать. Они раскрывают конфиденциальную информацию о токенах и сессиях. } }
Чтобы проверить работу авторизации, переходим по ссылке: https://test.indeed.local. Если все сделано правильно, в результате должно произойти внутреннее перенаправление на страницу входа.
Алгоритм отладки
Сложность отладки предаутентификации заключается в самой архитектуре этого механизма. Мы создали цепочку из независимых узлов, каждый из которых общается с другим через HTTP-запросы, редиректы и криптографические токены, так что приходилось методично по цепочке изучать поведение всех трех компонентов.
Сначала отключали Nginx и стучались напрямую на локальный порт 4180 (OAuth2 Proxy), чтобы проверить чистую схему авторизации и редиректы к IdP. Затем отключили провайдера и проверяли связку Nginx + Proxy.
Чтобы было проще, добавили в конфигурацию Nginx отладочные заголовки (они есть в конфиге выше, в секции DEBUG). Благодаря ним через инспектор браузера видно, какие именно данные (claims) прокси извлекает из JWT и передает балансировщику.
В итоге мы пришли к такому алгоритму:
Открываем консоль разработчика в браузере (вкладка Network), идем на целевой ресурс, и смотрим, куда нас отправил сервер. Получили ли мы 302 Found? Если да, то куда ведет этот редирект, на локальный прокси или сразу на Identity Provider?
Заглядываем в access.log и error.log самого Nginx, и ищем, увидел ли балансировщик наш изначальный запрос.
Переключаемся на логи OAuth2 Proxy. Что именно пришло к нему от Nginx? Увидел ли прокси куку авторизации в заголовках? Если куки нет, мы пытаемся понять логику демона: почему он решил перенаправить пользователя именно на этот конкретный URL провайдера (Indeed AM)?
Проверяем, произошла ли успешная авторизация в консоли Identity Provider. Выдал ли Indeed AM токен и отправил ли пользователя обратно на Callback URL прокси-сервера?
Возвращаемся в браузер и проверяем, работает ли авторизация. . Пользователь вернулся с токеном, но пустила ли его система? Если нет, начинаем цикл заново, только теперь ищем в логах прокси причину, по которой наш механизм отверг свежей выпущенную сессию.
Итоги и ограничения схемы
На дебаг этого механизма, дерганье портов и жонглирование заголовками ушло немало времени. В результате у нас получилась схема, которая в нашем случае заработала стабильно и оказалась жизнеспособной для корпоративного контура. Да, конструкция вышла многослойной и не самой изящной, но рабочей.
Вынеся аутентификацию за пределы приложения, мы получили механизм внешней предаутентификации для legacy-системы без масштабного рефакторинга. Он не заменяет встроенную модель авторизации на стороне приложения, но позволяет резко поднять уровень контроля доступа и внедрить MFA там, где сам бэкенд этого не умеет. Однако, за удобство приходится расплачиваться появлением новой точки отказа и усложнением отладки и сопровождения.
Если вам тоже приходилось решать похожую задачу, интересно сравнить подходы: выносили аутентификацию на периметр, закрывали legacy за VPN или все же шли в рефакторинг приложения? Напишите в комментариях, какой в вашем случае оказалась цена такого компромисса
