⚙️ Реализация Refresh Token
📦 Для хранения сессии я использую класс UserSession (1). В нём лежит вся ключевая информация: хэш токена, данные об устройстве, статус сессии и т.д.
🔑 При регистрации или авторизации запрос уходит в UserSessionService, где метод CreateSessionAsync (2) генерирует первичную пару
access и refresh токенов.🚦 Дальше каждый входящий запрос обрабатывается отдельным middleware - TokenRefreshMiddleware (3).
💡 И здесь кроется отличие от прошлого поста. Там клиент сам ловит ошибку 401 и отправляет повторный запрос на рефреш с фронта. Я решил избавить фронтенд от этой рутины: middleware сам достаёт оба токена из cookies и проверяет их прямо на бэкенде.
🔄 Если
access_token истёк, вызывается метод RefreshSessionAsync (4) у UserSessionService, который и запускает процесс ротации.⚙️ Вся логика ротации разделена на три возможных исхода:
🚨 1. Invalid
Токен не подошёл ни под текущий хэш, ни под предыдущий. Тут же срабатывает Token Reuse Detection (детект повторного использования старого токена). Мы отзываем сессию и блокируем доступ, защищая аккаунт от взлома.
✅ 2. ValidCurrent
Стандартная успешная ротация. Текущий токен переносится в PreviousTokenHash, генерируется новый секрет и обновляется срок жизни сессии.
⏳ 3. ValidGracePeriod
Тот самый исход, о котором я не говорил в прошлый раз, но без которого на практике сессия сломается.
❓ Зачем нужен Grace Period?
Когда пользователь открывает страницу, браузер может одновременно отправить 3-4 параллельных запроса. Первый запрос прилетает на бэк, успешно ротирует токен и обновляет куку. Но остальные запросы уже находятся в пути, и у них всё ещё старый refresh токен.
💀 Без Grace Period сервер посчитал бы эти параллельные запросы атакой (Invalid), и пользователя бы просто выкинуло из системы на ровном месте.
🛡 Благодаря короткому окну (буквально пара секунд) бэкенд разрешает использовать старый токен из PreviousTokenHash и спокойно отдаёт данные, не ломая активную сессию.
👌 На этом механизм ротации завершён.
👾 Полезное наблюдение: генерировать refresh токен в формате <SessionId>.<Secret>. Это позволяет при проверке моментально находить сессию по Id, а затем сверять хэш секрета.