1️⃣Auth / JWT

Приложение загружается. Me запрос отрабатывает, с колодами реализованы CRUD операции. Но сейчас мы имеет такое поведение кода связанного с заглушкой в headers

Удалим заглушку из flashcardsApi.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const flashcardsApi = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.flashcards.andrii.es',
  }),
  endpoints: () => ({}),
  reducerPath: 'flashcardsApi',
  tagTypes: ['Decks', 'Me'],
})

❌Соответственно видим что me запрос падает с ошибкой

  • Логинимся arrow-up-rightс тестовыми данными (test@test.com / test) и видим, что в респонсе нам возвращаются токены

✅ jwt

Поговорим теперь про JWT токеныarrow-up-right

JWT токен состоит из 3-х частей:

  • HEADER Заголовок обычно состоит из двух частей: типа токена, который является JWT, и используемого алгоритма подписи, например HMAC SHA256 или RSA.

  • PAYLOAD - данные о юзере Полезная нагрузка содержит утверждения. Утверждения - это утверждения о сущности (обычно, пользователе) и дополнительных данных.

  • VERIFY SIGNATURE (подпись). Чтобы создать подпись, берется закодированный заголовок, закодированная полезная нагрузка, секретный ключ и алгоритм, указанный в заголовке. Например, если используется HMAC SHA256, подпись создается следующим образом ✳️секретный ключ генерируется на бекенде ❗Подпись используется для проверки подлинности отправителя сообщения и целостности сообщения.

  • Давайте теперь попрактикуемся и сохраним токен в localstorage

Для того чтобы реализовать данную задачу нам понадобится onQueryStartedarrow-up-right

circle-info

onQueryStarted — это часть конфигурации в RTK Query, которая позволяет вам выполнять побочные эффекты и обрабатывать запросы до того, как они будут отправлены. Эта функция особенно полезна для оптимистичного обновления состояния или для выполнения других асинхронных операций, которые могут понадобиться до завершения запроса.

❗Ошибка в коде. Не указан response login endpoint

circle-info

queryFulfilled — это промис, возвращаемый RTK Query, который разрешается, когда запрос успешно завершен. Вы можете использовать queryFulfilled, чтобы выполнить дополнительные действия после успешного завершения запроса или для обработки ошибок, если запрос не удался.

удалите invalidatesTags: ['Me'], т.к. после логин запроса не надо делать me

После данного шага данные лягут в localstorage 🚀

  • Данные сохранились в localstorage. И что с того ? Нас не перекидывает на страницу колод. Нам нужно к каждому запросу цеплять access token

Существуют различные схемы Аутентификацииarrow-up-right

❗Мы будем использовать Bearer

circle-info

Bearer-токен — это тип авторизационного токена, который предоставляется пользователю после успешной аутентификации для доступа к защищенным ресурсам. Этот токен обычно представляет собой строку, которую пользователь должен передавать в заголовке HTTP-запроса для каждого запроса к защищенному сервису или приложению.

Bearer-токен используется для идентификации пользователя и предоставления доступа к его личным данным или другим защищенным ресурсам. Он должен храниться в безопасном месте и не передаваться третьим лицам.

Bearer-токен обеспечивает простой механизм аутентификации и авторизации пользователей без необходимости использования cookies или сессий. Он является частью стандарта аутентификации OAuth 2.0 и широко используется во многих современных веб-приложениях и API.

✳️prepareHeaders отрабатывает после каждого запроса, т.е. к каждому запросу будет цепляться Bearer token

После данного шага после логинизации мы должны оказаться на странице колод 🚀

✅ Безопасность

Хранение JWT (JSON Web Token) в localStorage имеет свои риски и преимущества. Вот основные моменты, которые нужно учитывать:

Преимущества:

  1. Простота использования: localStorage легко использовать и доступ к нему можно получить из любого места в вашем JavaScript коде.

  2. Долгосрочное хранение: Данные в localStorage сохраняются даже после закрытия вкладки или перезагрузки браузера, что делает его удобным для хранения токенов, которые не истекают быстро.

Риски:

  1. Уязвимость к XSS (Cross-Site Scripting): Если ваш сайт подвержен атакам XSS, злоумышленник может получить доступ к localStorage и украсть токен. XSS является одной из самых распространенных уязвимостей веб-приложений.

  2. Нет встроенной защиты: В отличие от HTTP-only cookies, которые не могут быть прочитаны JavaScript-ом, localStorage доступен из любого скрипта на странице.

Альтернативы:

  1. HTTP-only cookies: Хранение JWT в HTTP-only cookies уменьшает риск XSS, так как JavaScript не может получить доступ к таким cookies. Однако это делает вас уязвимым к атакам CSRF (Cross-Site Request Forgery), для защиты от которых нужно внедрять дополнительные меры (например, токены CSRF).

Рекомендации:

  1. Используйте HTTPS: Всегда используйте HTTPS для защиты данных в транзите.

  2. Проверяйте уязвимости XSS: Убедитесь, что ваш сайт не подвержен атакам XSS. Используйте Content Security Policy (CSP) для ограничения того, какие скрипты могут выполняться на вашей странице.

  3. Срок действия токена: Устанавливайте краткосрочный срок действия для токенов и используйте механизмы обновления (refresh tokens).

  4. Сегментируйте доступ: Ограничивайте область применения токенов до минимально необходимого уровня доступа.

Итог:

Хранение JWT в localStorage может быть приемлемым решением, если ваш сайт защищен от XSS и вы принимаете соответствующие меры предосторожности. Однако для лучшей безопасности рассмотрите использование HTTP-only cookies, особенно если у вас есть опыт защиты от CSRF атак.

Прекращение поддержки сторонних файлов cookiearrow-up-right

✳️ Если говорить простыми словами, то

  • плохо делать токен маложивущим, т.к. постоянно будет выкидывать пользователя

  • плохо делать токен долгоживущим, т.к. если токен украдут то им могут пользоваться условно неделю

Поэтому у нас 2 токена: access и refresh токены

  • access токен (токен доступа) как правило маложивущий 5-15 минут

  • refresh токен (токен обновления access токена)

  • access токен перехватить легко, т.к. он идет на каждый запрос, а refresh токен сложно, т.к. он 1 раз в 15 минут (когда тухнет access токен)

✅ Теория - работа с access / refresh токенами

Последовательность следующая:

  • идет первый запрос, возвращаются данные и access и refresh токены

  • проходит 15 минут / access протухает

  • вам приходит 401 с бекенда

  • вы автоматом перехватывает результат 401 запроса

  • делаете запрос на специальный endpoint arrow-up-rightс refresh токеном

  • и в ответ приходит новые access и refresh токены

  • вы берете новый access и сетаете его в localstorage

  • и повторяете тот запрос который упал

  • и естественно этот запрос пройдет 🚀

Итого: пользователь понятия не имеет, что у него какой-то запрос упал, потом еще раз пошел и со 2-го раза прошел. При этом пользователь в безопасности ценной лишь повторного запроса (refresh запрос всегда очень быстрый и простой)

И безопасно и пользователю нормально 🚀

✅Практика - работа с access / refresh токенами

Будем использовать вариант с async-mutexarrow-up-right для предотвращения многократных вызовов '/refreshToken', когда несколько вызовов завершаются с ошибкой 401 Unauthorized.

circle-info

async-mutex — это библиотека для управления синхронизацией в асинхронных JavaScript/TypeScript приложениях. Она предоставляет примитивы, такие как мьютексы и семафоры, которые помогают контролировать доступ к общим ресурсам, предотвращая условия гонки и обеспечивая корректное выполнение асинхронных операций. Mutex (Мьютекс): Используется для обеспечения эксклюзивного доступа к ресурсу. Только одна асинхронная функция может получить доступ к ресурсу в любой момент времени.

  • Создайте файл flashCardsBaseQuery.ts и скопируйте код arrow-up-right

  • вынесите fetchBaseQuery из flashcardsApi.ts в flashCardsBaseQuery.ts

  • Теперь давайте разбираться и подстраивать под свой код flashCardsBaseQuery.ts

baseQueryWithReauth вызывается на каждый запрос. Проверим это и посмотрим что нам на данные которые к нам приходят

Т.е. args - это аргументы, которые приходят в запрос

  • Следующая строка - это результат выполнения запроса

Всю эту движуху мы затеяли чтобы отлавливать 401 ошибку. Поэтому вылогинимся (зачистим localstorage) и посмотрим на результат

  • Следующая строка которую нужно подкорректировать

Для того, чтобы ее правильно написать посмотри на refresh token endpointarrow-up-right

  1. Прописываем аргументы согласно ендпоинту

  2. Добавляем в header запроса refresh token, который достаем из localstorage

Чтобы продемонстировать refreshResult нужно залогиниться, но не нажимать remember me

❗При rememberMe: false, acess токен протухает через 10 секунд. Сделано в целях тестирования

❗Необходимо добавить условие в fetchBaseQuery

  • Двигаемся дальше. Теперь нам необходимо положить в localstotage новые токены и повторить запрос

  • С типизацией придется мучаться т.к. библиотека RTK query не возвращает нам QueryReturnValue

  • чтобы сделать редирект на логин экспортируйте роутер

  • Когда будете делать logoutarrow-up-right, зачищаейте localstorage

И после этого все должно работать 🚀

Last updated