Где на клиенте следует хранить JWT-токен и какие у этого есть риски?

Ответ

JWT-токен хранится на стороне клиента. Выбор места хранения — это компромисс между удобством и безопасностью. Основные варианты и связанные с ними риски:

  1. localStorage / sessionStorage

    • Описание: Хранилище в браузере, доступное через JavaScript. sessionStorage очищается при закрытии вкладки.
    • Плюс: Простота реализации.
    • Минус: Уязвимость к XSS-атакам (Cross-Site Scripting). Если злоумышленник внедрит свой скрипт на страницу, он сможет прочитать и украсть токен.
      
      // Сохранение токена
      localStorage.setItem('jwt_token', 'your.jwt.token');

    // Получение токена для заголовка Authorization const token = localStorage.getItem('jwt_token');

  2. HttpOnly Cookie

    • Описание: Cookie, которые автоматически отправляются с каждым HTTP-запросом на тот же домен, но недоступны для чтения из JavaScript на клиенте.
    • Плюс: Встроенная защита от XSS-атак, так как скрипты не могут прочитать токен.
    • Минус: Уязвимость к CSRF-атакам (Cross-Site Request Forgery). Требует дополнительных мер защиты (атрибут SameSite=Strict или Lax, CSRF-токены).
      # Пример установки cookie в ответе (Flask)
      response.set_cookie(
      'access_token',
      'your.jwt.token',
      httponly=True,  # Запрещает доступ из JS
      secure=True,    # Отправка только по HTTPS
      samesite='Strict' # Защита от CSRF
      )

Рекомендуемый подход (Token-Based Authentication)

Наиболее безопасной считается комбинация из двух токенов:

  • Access Token (короткоживущий): Хранится в памяти приложения (например, в переменной JavaScript). Используется для авторизации запросов. Его короткий срок жизни (5-15 минут) минимизирует ущерб в случае кражи.
  • Refresh Token (долгоживущий): Хранится в HttpOnly cookie. Используется исключительно для получения нового access token, когда старый истекает.

Этот подход сочетает защиту от XSS (токен не в localStorage) и снижает риски CSRF (критичные операции требуют access token, который не отправляется автоматически как cookie).

Ответ 18+ 🔞

Вот, смотри, разберём эту всю хрень с хранением токенов, а то народ как обезьяны с гранатами — берут куда попало и потом удивляются, что всё взорвалось.

Ну, JWT-токен — это типа твой пропуск в клуб. Выдал тебе его швейцар (сервер), а ты теперь думаешь, куда его сунуть, чтобы и не потерять, и чтобы тебя не обнесли по карманам.

Вариант первый — запихнуть в localStorage или sessionStorage.
Это как оставить ключи от квартиры на видном месте в прихожей.
Плюс: дохуя просто, открыл шкафчик (localStorage.setItem) — положил, достал — пошёл.
Минус: любой скрипт, который проберётся на твою страницу (это и есть XSS-атака), спокойно выковыряет твои ключи и сделает дубликат. Пипец твоей квартире.

// Вот так ты радостно его туда пихаешь
localStorage.setItem('jwt_token', 'your.jwt.token');
// А вот так какой-нибудь скрипт-гадёнок его вытащит
const stolenToken = localStorage.getItem('jwt_token');

Вариант второй — HttpOnly куки.
Это уже умнее. Токен лежит в специальной банке, которую браузер сам приносит серверу с каждым запросом, но твои скрипты туда залезть не могут.
Плюс: от XSS защита на уровне, потому что JS его не прочитает.
Минус: появляется другая засада — CSRF. Если ты зайдёшь на левый сайт, а он тебе подсунет форму, которая стукнет на твой же банковский сайт, браузер автоматом прицепит эту куку, и деньги могут уплыть. Но от этого есть противоядие — флаги Secure, SameSite.

# Сервер тебе такую куку в ответ суёт
response.set_cookie(
    'access_token',
    'your.jwt.token',
    httponly=True,  # Руки прочь, JavaScript!
    secure=True,    # Только по HTTPS, никакого говнотрафика
    samesite='Strict' # Не цепляйся к запросам с левых сайтов
)

А теперь, блядь, рекомендуемый подход, чтобы спать спокойно.
Используй два токена, как в хорошем банке: один — для повседневных трат, второй — для возобновления первого, если тот сдох.

  1. Access Token (короткоживущий, 5-15 минут).
    Храни его в памяти приложения (просто в переменной JS). Он как наличка в кармане — украли, потратили, но ненадолго. Каждый запрос к API цепляешь его в заголовок Authorization. Украсть его сложнее, а если и украдут — он быстро протухнет.

  2. Refresh Token (долгоживущий, дни/недели).
    Вот его-то и пихай в HttpOnly куку. Он нужен только для одной операции — прийти к серверу и сказать: «Э, дружок, access token мой сдох, выдай новый». Поскольку из JS его не вытащить, то и скомпрометировать его овердохуища сложно.

Получается такая схема: XSS-атака не сожрёт refresh token (он в куке), а CSRF-атака не сделает ничего серьёзного, потому что для важных операций нужен access token из памяти, который кукой не отправляется. Красота, ёпта!

Вот так и живём: не кладём все яйца в одну корзину, а то охуеем потом, когда она провалится.