В одном из проектов мне понадобилось отслеживать состояние SSO-сессии на уже открытой странице. Пользователь выходит из SSO через другое приложение, а наш фронт продолжает показывать прежний интерфейс. Хотелось, чтобы страница сама узнавала об изменении и подтягивала актуальное состояние пользователя.
Механизм описан в OpenID Connect Session Management 1.0. Дальше расскажу, как я встраивал этот механизм в проект на React и Django с Keycloak.
Как я это реализовал
Авторизация в приложении уже работала: браузер уходил в Keycloak, возвращался на callback, backend создавал Django сессию. Полученный при входе session_state сохранялся на сервере. Его и предстояло передавать на фронт для последующих проверок.
На фронте я вынес мониторинг в отдельный React-компонент SsoSessionManager. Он подключается при наличии авторизованного пользователя, ничего не рисует и управляет двумя скрытыми iframe. Один живёт всё время работы manager, второй появляется только для уточнения состояния сессии. Авторизация и работа с токенами оставались на бэкенде, а фронту требовались конфигурация проверки и её результат.
Чтобы фронт мог получить контекст мониторинга, а backend - отдельно от обычного логина уточнить SSO-сессию и обработать результат, я добавил три новые ручки: GET /api/auth/sso-session отдаёт конфигурацию и сохранённый session_state, GET /auth/sso-session/start подготавливает временный флоу авторизации и перенаправляет iframe в Keycloak, а GET /auth/sso-session/callback обрабатывает ответ Keycloak и возвращает HTML во временный iframe. Скрипт этого документа передаёт результат проверки родительской странице через postMessage.
Постоянный iframe загружает login-status-iframe.html из Keycloak. После загрузки SsoSessionManager отправляет в него через postMessage строку client_id + " " + session_state. Пока вкладка скрыта, опрос SSO приостанавливается, а при возвращении проверка запускается снова. Всего, при опросе SSO, может вернуться 3 состояния: При unchanged продолжаем проверять. При changed останавливаем опрос и создаём временный iframe с адресом /auth/sso-session/start. Сам по себе changed ещё не сообщает, нужно ли завершать локальную сессию, поэтому дальше разбирается backend. При error manager отключает мониторинг для текущей страницы.
Ручка /start проверяет локальную сессию, создаёт одноразовые state и nonce, сохраняет ожидаемого пользователя. Затем перенаправляет временный iframe на авторизационную ручку Keycloak с prompt=none. Этот параметр запрещает показывать форму логина и другие интерактивные экраны. Keycloak пытается выполнить запрос на основе уже существующего входа в браузере: если это возможно, возвращает code, но если нужно участие пользователя, то возвращает ошибку. Для нас это способ уточнить состояние SSO в скрытом iframe, не уводя пользователя с открытой страницы и не заставляя его снова вводить пароль.
Чтобы уточнить состояние входа в Keycloak без участия пользователя, SsoSessionManager задаёт временному iframe src="/auth/sso-session/start". Браузер проходит внутри этого iframe цепочку редиректов для проверки входа в Keycloak, а /auth/sso-session/callback отдаёт небольшой HTML со скриптом, вызывающим window.parent.postMessage, который отправляет результат родительской странице.
Когда Keycloak возвращает code на /auth/sso-session/callback, Django обменивает его на токены отдельным серверным запросом. Backend делает необходимые проверки, затем сверяет sub с ожидаемым и текущим локальным пользователем. Если пользователь тот же, то сохраняется новый session_state. Инные случаи приводят к завершению локальной сессии. SsoSessionManager проверяет origin, окно-источник и структуру сообщения. Если пришёл session_updated, забирает новый session_state, удаляет временный iframe и возобновляет опрос SSO. Если logged_out - отключает мониторинг и обновляет страницу.