Метод POST /api/v1/auth/refresh

Документация API: Фоновое обновление сессии (Refresh Token) с бесшовным перевыпуском Multi-Tenancy JWT Claims

Published

July 2, 2026

1 Функциональное назначение

Метод предназначен для безопасного продолжения активной пользовательской сессии без повторного принудительного ввода логина и пароля на смартфоне.

В рамках микросервисной архитектуры этот эндпоинт решает критические инфраструктурные задачи:

  1. Обслуживание Dio Interceptor: Метод вызывается сетевым слоем Flutter-приложения строго в фоновом режиме, когда шлюз возвращает ошибку HTTP 401 Unauthorized на любой транзакционный запрос.
  2. Бесшовный перевыпуск контекста: Проверяет долгоживущий Refresh Token по базе сессий и генерирует новый короткоживущий Access Token (JWT), сохраняя актуальные JWT Private Claims home_group_id, account_type и user_id. Это гарантирует, что фоновые процессы сокетов и транзакции не прервутся для пользователя.
  3. Механизм обновления токенов (Refresh Token Rotation): Для защиты от компрометации, при каждом вызове старый Refresh-токен аннулируется, а клиенту выдается новая пара токенов, что предотвращает атаки типа Replay Attack.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: POST
  • Маршрут: /api/v1/auth/refresh
  • Формат данных: application/json

2.1 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
X-Request-ID Да Сквозной ID запроса для трассировки фоновой ротации сессии req-auth-ref-22aa

2.2 Спецификация тела запроса (Request Body)

В соответствии с требованиями безопасности, Refresh-токен передается в теле JSON-запроса (или извлекается из защищенных HttpOnly Cookies в зависимости от конфигурации окружения).

Поле Тип Обязательный Описание Пример значения
refresh_token String Да Долгоживущий токен обновления, выданный при входе в систему ref-token-xyz789...

2.2.1 Пример сырого JSON-запроса (Payload):

{
  "refresh_token": "ref-token-xyz789_v1_signature..."
}

2.3 Спецификация успешного ответа (Response Body)

2.3.1 HTTP 200 OK

Возвращается при успешной валидации сессии. Выдается обновленный JWT-токен.

{
  "status": "success",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3UrODgyMiIsImhvbWVfZ3JvdXBfaWQiOiJncm91cC03NzItYWxwaGEiLCJhY2NvdW50X3R5cCI6IkhPTUUiLCJleHAiOjE3ODMzMTMyMDB9.new_signature...",
  "expires_in": 3600
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлена логика работы Dio Interceptor при фоновом обновлении токенов. Сервис Auth проверяет refresh_token по базе активных сессий, выполняет обновление, аннулирует старый ключ и выпускает новый JWT с сохранением JWT Private Claims.

sequenceDiagram
    autonumber
    
    actor App as APP (Мобильное приложение)
    participant Nginx as Nginx Proxy
    participant GW as GATEWAY (backend-api)
    participant Auth as AUTH (auth-service)
    participant Redis as REDIS (Token Blacklist)
    participant DB as DB_AUTH (PostgreSQL)

    App->>Nginx: Шаг 1: POST /api/v1/auth/refresh (refresh_token)
    activate Nginx
    Nginx->>GW: Шаг 2: POST /backend-api/v1/auth/refresh
    activate GW
    GW->>Auth: Шаг 3: gRPC: VerifyAndRotate(RefreshRequest)
    activate Auth
    
    Auth->>Redis: Шаг 4: Redis.EXISTS(revoked_refresh)
    activate Redis
    note over Auth, Redis: При падении Redis:<br/>Fail-Close -> Аварийный выход HTTP 503
    Redis-->>Auth: Шаг 5: Токен НЕ найден в черном списке
    deactivate Redis
    
    Auth->>DB: Шаг 6: SQL SELECT is_revoked FROM user_sessions...
    activate DB
    DB-->>Auth: Токен валиден, срок не истек
    deactivate DB
    
    note over Auth: Шаг 7: Внутреннее действие:<br/>Атомарная деактивация старого<br/>и генерация нового токена
    
    Auth->>DB: Шаг 7 (продолжение): SQL UPDATE и INSERT сессий
    activate DB
    DB-->>Auth: Сессия обновлена
    deactivate DB
    
    Auth-->>GW: Шаг 8: gRPC: RotateResponse
    deactivate Auth
    GW-->>App: Шаг 9: HTTP 200 OK (new access, new refresh)
    deactivate GW
    deactivate Nginx
    
    note over App: Шаг 10: Внутренний метод<br/>SecureStorage.write(...) и разморозка очереди

Процесс ротации токенов (Refresh)

4 Расшифровка шагов и логика ротации токенов

Шаг Действие Параметры Ошибки (Исключения / Статусы)
Шаг 1 (APP -> NGINX) Перехватчик приложения (Dio Interceptor), поймав HTTP 401, замораживает очередь, достает из хранилища refresh_token и отправляет запрос ротации. HTTP POST /api/v1/auth/refresh
Body: {"refresh_token": "..."}
DioException: connection timeout
Шаг 2 (NGINX -> GATEWAY) Nginx пересылает запрос на внутренний API Gateway. HTTP POST /backend-api/v1/auth/refresh HTTP 502 Bad Gateway
Шаг 3 (GATEWAY -> AUTH) API Gateway проверяет контракт и вызывает внутренний gRPC-метод ротации сессии. gRPC: VerifyAndRotate(RefreshRequest) HTTP 422 Unprocessable Entity
Шаг 4 (AUTH -> REDIS) auth-service проверяет, не находится ли входящий Refresh Token в черном списке отозванных сессий. Вызов: Redis.EXISTS(f"revoked_refresh:{jti}") Redis: ConnectionRefusedError
gRPC: UNAVAILABLE \(\rightarrow\) Fail-Close! HTTP 503
Шаг 5 (REDIS -> AUTH) Redis возвращает статус проверки. Если токен в черном списке — сессия считается скомпрометированной. Результат проверки в Redis gRPC: UNAUTHENTICATED
HTTP 401 / IAD_JWT_REFRESH_STOLEN
Шаг 6 (AUTH -> DB_AUTH) Сервис проверяет валидность сессии и ее срок годности (expires_at) в транзакционной базе данных. SQL SELECT is_revoked FROM user_sessions WHERE refresh_token_jti = $1 gRPC: UNAUTHENTICATED
HTTP 401 / IAD_JWT_REFRESH_EXPIRED
Шаг 7 (AUTH -> DB_AUTH) Внутреннее действие: Сервис атомарно деактивирует старый Refresh Token и записывает данные новой сессии для защиты от повторного использования токена. SQL UPDATE user_sessions ...
SQL INSERT INTO user_sessions ...
PostgreSQL: transaction_deadlock
gRPC: ABORTED \(\rightarrow\) HTTP 410 Gone
Шаг 8 (AUTH -> GATEWAY) Сервис возвращает новую сгенерированную пару токенов на API Gateway по gRPC. gRPC: RotateResponse gRPC: INTERNAL
Шаг 9 (GATEWAY -> APP) API Gateway передает обновленные данные через Nginx обратно в мобильное приложение. HTTP 200 OK
Body: {"access_token": "...", "refresh_token": "..."}
HTTP 500 Internal Server Error
Шаг 10 (APP -> APP) Приложение обновляет ключи в secure storage, размораживает очередь и прозрачно перезапускает упавший пользовательский запрос. Вызов: SecureStorage.write(...) SecureStorageException

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 1. Спецификация структуры сессий PostgreSQL (Token Rotation Layer)

Для поддержки безопасного механизма обновления токенов и определения повторного использования угнанных ключей (Reuse Detection), база данных хранит метаданные сессий в изолированной таблице.

-- Таблица сессий и долгоживущих Refresh-токенов
CREATE TABLE user_sessions (
    id BIGSERIAL PRIMARY KEY,
    user_id VARCHAR(50) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token VARCHAR(512) NOT NULL UNIQUE,       -- Хэш или тело Refresh-токена
    is_revoked BOOLEAN NOT NULL DEFAULT FALSE,-- Флаг аннулирования токена (использован/украден)
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Индекс для мгновенной верификации токена при фоновом обновлении Dio Interceptor
CREATE INDEX idx_user_sessions_lookup ON user_sessions(token) WHERE is_revoked = FALSE;

5.2 2. Вилки исключений и обработка ошибок

5.2.1 Ошибка: Токен просрочен или аннулирован (HTTP 401 Unauthorized — Шаг 9)

Выбрасывается на Шаге 9, если refresh_token истек по времени жизни, либо если сработал триггер детекции повторного использования (is_revoked == true). Это жесткий сигнал для Flutter полностью сбросить локальный кэш и принудительно выкинуть пользователя на экран авторизации.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "error_code": "ERR-REFRESH-TOKEN-EXPIRED",
  "message": "Сессия авторизации полностью истекла или была аннулирована из соображений безопасности. Необходим повторный вход.",
  "details": {
    "action": "Clear Flutter Secure Storage, disconnect WebSockets, and route user to Login Screen."
  }
}

5.2.2 Ошибка: Токен поврежден или имеет неверный формат (HTTP 422 Unprocessable Entity)

Выбрасывается бэкенд-шлюзом FastAPI, если переданная строка в поле refresh_token пустая или нарушает валидацию длины Pydantic-модели.

{
  "error_code": "ERR-VALIDATION-FAILED",
  "message": "Передан некорректный или пустой токен обновления refresh_token.",
  "details": [
    {
      "loc": ["body", "refresh_token"],
      "msg": "Field constraints violation: token string cannot be empty",
      "type": "value_error.str.min_length"
    }
  ]
}