Объяснение токенов обновления Keycloak и времени истечения сессий
Почему при обновлении в Keycloak возвращается код 400, в чём разница между временем ожидания активности сессии и максимальным временем существования сессии, и как использовать токены обновления с защитой от одновременного использования с хранилищем, устойчивым к их ротации, в JavaScript.
Источники: Refresh Tokens and Session Timeouts in Keycloak: Why a 400 Isn’t Always a Bug
Приложения, основанные на Keycloak, иногда получают от конечной точки токенов ответ 400 Bad Request во время обновления. На первый взгляд это указывает на сбой процесса обновления. Чаще всего Keycloak отказывает намеренно: сессия вышла из активного состояния, достигла максимального срока действия, была аннулирована или токен обновления больше не действителен. Такие ситуации являются результатом настроек политик, а не дефектов.
В этом руководстве рассматриваются все важные аспекты работы в производственной среде: функции токенов доступа и обновления, полный процесс их обновления, критерии успешного обновления сессии, а также паттерны и антипаттерны работы фронтенда с остальными аспектами.
Почему существует эта статья
Когда конечная точка токенов возвращает код 400, команды часто регистрируют это как баг с авторизацией. Первый важный вопрос — какое правило срока действия или аннулирования токена сработало. Понимание этого позволяет не ошибочно считать законный отказ сбоем.
1. Два токена, две совершенно разные функции
После входа Keycloak обычно выдает токен доступа, токен обновления и токен ID. Ежедневная работа зависит от первых двух.
- Токен доступа — прилагается к обычным запросам API для подтверждения того, кто осуществляет вызов. Срок его действия короткий, обычно измеряется в минутах.
- Токен обновления — отправляется только в Keycloak для получения нового токена доступа. Срок его действия длиннее, обычно тридцать минут или больше.
- Офлайн-токен — необязательный токен длительного срока действия, который может превышать срок обычной сессии SSO; его использование регулируется отдельной политикой.
Простая аналогия: токен доступа — это временный пропуск, который показывают у каждых дверей. Токен обновления — это документ, заменяющий пропуск; его никогда не показывают API приложений, и он должен поступать только на конечную точку токенов Keycloak.
2. Обычный процесс
При входе в систему возвращается набор токенов. После этого клиент прикрепляет токен доступа к каждому запросу:
GET /api/products
Authorization: Bearer eyJ...
Срок действия токенов короткий — обычно от пяти до пятнадцати минут. Если вход осуществляется в 09:00 с токеном доступа сроком действия в пять минут, к 09:05 следующий запрос к API вернёт код 401 Unauthorized.
Этот код 401 не означает немедленный выход из системы. Это сигнал о необходимости обновления токенов.
3. Сам запрос на обновление
Клиент отправляет запрос на конечную точку токенов Keycloak с токеном обновления:
POST /realms/{realm}/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
client_id=<client-id>
refresh_token=<refresh-token>
Здоровая структура запроса выглядит так:
{
"access_token": "new-access-token",
"expires_in": 300,
"refresh_token": "new-refresh-token",
"refresh_expires_in": 1800
}
Клиент сохраняет новый токен доступа, повторно отправляет первоначальный запрос, и пользователь не замечает этого процесса.
4. Подробности ротации: каждый раз заменяйте сохранённые учетные данные для обновления
В ответе также часто содержится новый токен обновления. Когда включена функция обновления токенов обновления, предыдущий токен аннулируется сразу после выдачи нового. Повторное использование старого значения приводит к неудаче при следующем обновлении.
// Correct
saveAccessToken(response.access_token);
if (response.refresh_token) {
saveRefreshToken(response.refresh_token);
}
// Wrong — keeps using the token that rotation just invalidated
const refreshToken = originalRefreshToken;
Сохранение обновленного значения токена обновления после каждого успешного действия — а не только после входа — и определяет, будет ли сессия продолжать работать без проблем в течение часов или же сразу сорвется при второй попытке.
5. Idle versus Max: таймеры, контролирующие обновление
В системах Realm предусмотрены по меньшей мере два таймера сессии, и изменение их значений является причиной большинства запросов типа «почему произошла ошибка аутентификации»:
- SSO Session Idle — срок, в течение которого сессия может находиться в состоянии без действий, прежде чем Keycloak её завершит. Успешное обновление токена считается действием и сбрасывает этот таймер.
Короче говоря:
IDLE = how long since the session was last active
MAX = how long since the user originally logged in
При настройках Idle = 30 минут и Max = 16 часов частые обновления страницы позволяют держать таймер бездействия в активном состоянии весь день, но через 16 часов с момента входа сессия заканчивается, даже если пользователь был активен всего минуту ранее. Идеальное время обновления не может превысить установленный лимит; оно лишь сохраняет сессию внутри этого временного окна.
Параметры Idle/Max для каждого клиента отражают настройки сервера, но только для данного клиента. Применяется более строгий из этих лимитов.
Почему в примерах указан максимум в шестнадцать часов: команда ERP с сменой примерно в двенадцать часов может выбрать двенадцать часов работы плюс четырехчасовой резерв, чтобы сессия сохранялась в течение всего дня без необходимости повторной авторизации посреди задачи и в то же время не длилась бесконечно. Это деловой выбор; важной навыком является понимание того, какой механизм времени он контролирует.
Почему даже тридцатиминутный период бездействия всё ещё удивляет людей: даже при щедром максимуме длительный обед или совещание могут привести к раннему окончанию периода бездействия. Если пользователи должны вернуться без повторной аутентификации, вопрос периода бездействия следует рассматривать как принятие решения по продукту/безопасности, а не просто как что-то, что нужно скрыть в коде клиента.
6. Загрузка данных 400 из конечной точки токена без предположения о сбое
Неудачная попытка обновления может выглядеть так:
{
"error": "invalid_grant",
"error_description": "Token is not active"
}
Этот пакет данных может указывать на несколько разных ситуаций:
- Аутентификационные данные истекли сами по себе
Ни один из этих случаев не означает «API недоступен». Прочитайте поля error и error_description, прежде чем делать другие выводы.
7. Правильная обработка на фронтенде
Надёжный подход:
401 on API request
→ try refresh, exactly once
→ success: save new tokens, retry the original request
→ failure: clear auth state, redirect to login
Не пытайтесь восстанавливать сессию бесконечно: 401 — обновление, 400 — снова обновление. Одна неработающая сессия может привести к потоку запросов на получение токена и к постоянно вращающемуся индикатору загрузки, который так и не прекратит свою работу. Считайте неудачу обновления окончательной: удаляйте учетные данные, сбрасывайте состояние аутентификации, направляйте пользователя на вход и прекращайте дальнейшие попытки.
8. Для множественных запросов с кодом 401 требуется одно общее обновление
Панель управления, отправляющая пять запросов одновременно с истекшим токеном доступа, получает пять ответов с кодом 401. Если каждый путь обновляется независимо в рамках ротации, первый успешный запрос аннулирует токен, который все ещё хранят остальные четыре — в результате четыре попытки обновления завершаются с ошибкой 400 invalid_grant, хотя сама сессия была в порядке.
Сериализуйте процесс обновления: первый ответ с кодом 401 создаёт одно обещание; остальные запросы ожидают это же обещание и пытаются снова после его выполнения.
if (tokenExpired) {
if (!refreshInProgress) {
refreshInProgress = refreshToken();
}
try {
const tokens = await refreshInProgress;
updateTokens(tokens);
return retryOriginalRequest();
} finally {
refreshInProgress = null;
}
}
Независимо от того, является ли механизм блокировки реализован через вручную созданное обещание, очередь интерцепторов Axios или обёртку baseQuery из RTK Query, правило остаётся неизменным: одно обновление в процессе выполнения, независимо от того, сколько запросов ожидают.
9. Настройки, которые кажутся связанными, но не являются таковыми: время ожидания входа и время ожидания действия при входе
Ещё два параметра реальмов легко путать с временем жизни сессии, однако они влияют только на процедуру входа:
- Время ожидания входа — сколько времени может оставаться открытым весь процесс аутентификации (учетные данные, MFA, необходимые действия).
- Время ожидания действия входа — сколько времени может оставаться без ответа отдельный шаг (например, экран с кодом OTP).
Ни один из этих параметров не продлевает уже аутентифицированную сессию. Они важны только тогда, когда пользователь ещё в процессе входа.
10. Офлайн-сессии — инструмент для другой проблемы
Реальмы также могут создавать офлайн-сессии с помощью параметра scope=offline_access. Они позволяют получать новые токены доступа даже после окончания обычной сессии SSO — их цель — сохранить авторизацию на завтра и на следующую неделю, а не лишь на один рабочий день.
Для обычного использования в рабочее время (одна смена плюс резервное время) офлайн-доступ обычно не требуется. Настройка параметра Session Max делает решение проще и более надежным. Обращайтесь к офлайн-режиму только тогда, когда действительно необходима сохранность данных в течение дней или недель.
11. Чек-лист для отладки: почему при обновлении возвращается код 400
Выполняйте действия в следующем порядке:
- Прочитайте значения
errorиerror_descriptionв ответе. - Сравните время входа с значением
Session Maxна уровне серверной среды и клиента. - Сравните период бездействия с значением
Session Idleна уровне серверной среды и клиента. - Проверьте настройки клиента в разделе
Clients → <client> → Advanced. - Убедитесь, что включена ротация токенов обновления.
- Проверьте, не отправлялись ли одновременно несколько запросов на обновление.
- Убедитесь, что клиент сохраняет самый свежий токен обновления после каждого обновления, а не только первый.
12. Ментальная модель, которую следует сохранять
Три уровня, при этом зависимости протекают в одном направлении:
Access token → used for normal API calls
Refresh token → used to get a new access token
Keycloak session (Idle + Max) → decides whether that refresh is allowed at all
Токен обновления никогда не может существовать независимо от своей сессии. Если сессия утеряна — из-за длительного бездействия, истечения максимального срока действия, аннулирования или выхода из системы — никакой токен обновления не сможет её восстановить. Такое отклонение является проявлением корректной работы системы.
Чек-лист
- Рассматривайте истечение срока действия токена доступа отдельно от завершения сессии — попробуйте выполнить одну попытку обновления, прежде чем считать ошибку 401 выходом из системы.
- Сохраняйте обновлённые учетные данные для обновления на каждом успешном шаге, а не только новый токен доступа JWT.
- Помните о двух таймерах: один обновляется при активности, другой отсчитывает максимальный срок действия с момента входа.
- Сделайте неудачу при обновлении окончательной — одна попытка, затем удаляйте учетные данные и направляйте пользователя на вход; никогда не повторяйте попытки в цикле.