# Auth / JWT

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

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

```tsx
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 запрос падает с ошибкой

* [Логинимся ](https://api.flashcards.andrii.es/reference#tag/auth/post/v1/auth/login)с тестовыми данными (<test@test.com> / test) и видим, что в респонсе нам возвращаются токены

<div align="left"><figure><img src="https://content.gitbook.com/content/jRpDZ5OOulcxs84wk3X1/blobs/KYC1Mr5yNuW4SbSkHl9Z/image.png" alt=""><figcaption></figcaption></figure></div>

### ✅ jwt

Поговорим теперь про [JWT токены](https://jwt.io/)&#x20;

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

* HEADER\
  Заголовок обычно состоит из двух частей: типа токена, который является JWT, и используемого алгоритма подписи, например HMAC SHA256 или RSA.<br>
* PAYLOAD - данные о юзере\
  Полезная нагрузка содержит утверждения. Утверждения - это утверждения о сущности (обычно, пользователе) и дополнительных данных.<br>
* VERIFY SIGNATURE (подпись). \
  Чтобы создать подпись, берется закодированный **заголовок**, закодированная **полезная нагрузка**, секретный ключ и алгоритм, указанный в заголовке. Например, если используется HMAC SHA256, подпись создается следующим образом\
  \
  ✳️секретный ключ генерируется на бекенде\
  \
  ❗Подпись используется для проверки подлинности отправителя сообщения и целостности сообщения.

<figure><img src="https://content.gitbook.com/content/jRpDZ5OOulcxs84wk3X1/blobs/NMr9oGEi3gA53WUFeWIS/image.png" alt=""><figcaption></figcaption></figure>

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

Для того чтобы реализовать данную задачу нам понадобится [onQueryStarted](https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted)

{% hint style="info" %}
**`onQueryStarted`** — это часть конфигурации в RTK Query, которая позволяет вам выполнять побочные эффекты и обрабатывать запросы до того, как они будут отправлены. Эта функция особенно полезна для оптимистичного обновления состояния или для выполнения других асинхронных операций, которые могут понадобиться до завершения запроса.
{% endhint %}

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

{% code title="auth.types.ts" %}

```tsx
export type LoginResponse = {
  accessToken: string
  refreshToken: string
}
```

{% endcode %}

{% hint style="info" %}
**`queryFulfilled`** — это промис, возвращаемый RTK Query, который разрешается, когда запрос успешно завершен. Вы можете использовать `queryFulfilled`, чтобы выполнить дополнительные действия после успешного завершения запроса или для обработки ошибок, если запрос не удался.
{% endhint %}

```tsx
export const authService = flashcardsApi.injectEndpoints({
  endpoints: builder => ({
    login: builder.mutation<LoginResponse, LoginArgs>({
      //  invalidatesTags: ['Me'],
      async onQueryStarted(
        // 1 параметр: QueryArg - аргументы, которые приходят в query
        _,
        // 2 параметр: MutationLifecycleApi - dispatch, queryFulfilled, getState и пр.
        // queryFulfilled - это промис, возвращаемый RTK Query, который разрешается,
        // когда запрос успешно завершен
        { queryFulfilled }
      ) {
        const { data } = await queryFulfilled

        if (!data) {
          return
        }

        localStorage.setItem('accessToken', data.accessToken)
        localStorage.setItem('refreshToken', data.refreshToken)
      },
      query: body => {
        return {
          body,
          method: 'POST',
          url: '/v1/auth/login',
        }
      },
    }),
  }),
})


```

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

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

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

Существуют различные [схемы Аутентификации](https://developer.mozilla.org/ru/docs/Web/HTTP/Authentication#%D1%81%D1%85%D0%B5%D0%BC%D1%8B_%D0%B0%D1%83%D1%82%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8)

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

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

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

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

<pre class="language-tsx"><code class="lang-tsx">export const flashcardsApi = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://api.flashcards.andrii.es',
<strong>    prepareHeaders: headers => {
</strong><strong>      const token = localStorage.getItem('accessToken')
</strong>
<strong>      if (token) {
</strong><strong>        headers.set('Authorization', `Bearer ${token}`)
</strong><strong>      }
</strong>
<strong>      return headers
</strong>    },
  }),
  endpoints: () => ({}),
  reducerPath: 'flashcardsApi',
  tagTypes: ['Decks', 'Me'],
})

</code></pre>

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

<figure><img src="https://3919421388-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjRpDZ5OOulcxs84wk3X1%2Fuploads%2F7zV5Cjn891MVu1mUEgMs%2Fimage.png?alt=media&#x26;token=21d5a25e-927b-40d3-8d42-42036eb4424d" alt=""><figcaption></figcaption></figure>

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

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

Хранение 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 атак.

❗ [Прекращение поддержки сторонних файлов cookie](https://developers.google.com/privacy-sandbox/3pcd?hl=ru)

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

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

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

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

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

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

* идет первый запрос, возвращаются данные и access и refresh токены
* проходит 15 минут / access протухает
* вам приходит 401 с бекенда
* вы автоматом перехватывает результат 401 запроса
* делаете запрос на специальный [endpoint ](https://api.flashcards.andrii.es/reference#tag/auth/post/v2/auth/refresh-token)с **refresh токеном**
* и в ответ приходит новые **access и refresh токены**
* вы берете новый **access**  и сетаете его в `localstorage`
* и повторяете тот запрос который упал
* и естественно этот запрос пройдет 🚀

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

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

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

#### 🔗 [Automatic re-authorization by extending fetchBaseQuery](https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#automatic-re-authorization-by-extending-fetchbasequery) <a href="#automatic-re-authorization-by-extending-fetchbasequery" id="automatic-re-authorization-by-extending-fetchbasequery"></a>

Будем использовать вариант с [async-mutex](https://github.com/DirtyHairy/async-mutex) для предотвращения многократных вызовов '/refreshToken', когда несколько вызовов завершаются с ошибкой 401 Unauthorized.

{% hint style="info" %}
`async-mutex` — это библиотека для управления синхронизацией в асинхронных JavaScript/TypeScript приложениях. Она предоставляет примитивы, такие как мьютексы и семафоры, которые помогают контролировать доступ к общим ресурсам, предотвращая условия гонки и обеспечивая корректное выполнение асинхронных операций.\
\
**Mutex (Мьютекс)**: Используется для обеспечения эксклюзивного доступа к ресурсу. Только одна асинхронная функция может получить доступ к ресурсу в любой момент времени.<br>
{% endhint %}

* Создайте файл `flashCardsBaseQuery.ts` и скопируйте [код ](https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#preventing-multiple-unauthorized-errors)
* вынесите `fetchBaseQuery из flashcardsApi.ts` в `flashCardsBaseQuery.ts`

{% code title="flashCardsBaseQuery.ts" %}

```tsx
const baseQuery = fetchBaseQuery({
  baseUrl: 'https://api.flashcards.andrii.es',
  prepareHeaders: headers => {
    const token = localStorage.getItem('accessToken')

    if (token) {
      headers.set('Authorization', `Bearer ${token}`)
    }

    return headers
  },
})
```

{% endcode %}

<pre class="language-tsx" data-title="flashcardsApi .ts"><code class="lang-tsx">import { baseQueryWithReauth } from '@/services/flashCardsBaseQuery'
import { createApi } from '@reduxjs/toolkit/query/react'

export const flashcardsApi = createApi({
<strong>  baseQuery: baseQueryWithReauth,
</strong>  endpoints: () => ({}),
  reducerPath: 'flashcardsApi',
  tagTypes: ['Decks', 'Me'],
})

</code></pre>

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

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

<pre class="language-tsx"><code class="lang-tsx">export const baseQueryWithReauth: BaseQueryFn&#x3C;
  FetchArgs | string,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
<strong>  console.log({ api, args })
</strong>  ...
  }
</code></pre>

<div align="left"><figure><img src="https://3919421388-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjRpDZ5OOulcxs84wk3X1%2Fuploads%2FdUP7Kuh5vemHDmn8vPwP%2Fimage.png?alt=media&#x26;token=6c4dbfa2-19bd-42bd-85b6-4e6708e4bd89" alt=""><figcaption></figcaption></figure></div>

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

{% tabs %}
{% tab title="decks.service.ts" %}

<pre class="language-tsx"><code class="lang-tsx">    getDecks: builder.query&#x3C;DecksResponse, GetDecksArgs | void>({
      providesTags: ['Decks'],
      query: args => {
<strong>        return {
</strong><strong>          params: args ? getValuable(args) : undefined,
</strong><strong>          url: `v1/decks`,
</strong><strong>        }
</strong>      },
    }),
</code></pre>

{% endtab %}

{% tab title="auth.service.ts" %}

<pre class="language-tsx"><code class="lang-tsx">me: builder.query&#x3C;User, void>({
      providesTags: ['Me'],
<strong>      query: () => '/v1/auth/me',
</strong>    }),
</code></pre>

{% endtab %}
{% endtabs %}

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

```tsx
let result = await baseQuery(args, api, extraOptions)
console.log('result', result)
```

<div align="left"><figure><img src="https://3919421388-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjRpDZ5OOulcxs84wk3X1%2Fuploads%2F540AU4ZCu1DCNhbhrZgx%2Fimage.png?alt=media&#x26;token=ce541fc4-e88e-46cc-b1b2-3fc43068264f" alt=""><figcaption></figcaption></figure></div>

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

<div align="left"><figure><img src="https://3919421388-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjRpDZ5OOulcxs84wk3X1%2Fuploads%2F7enT22Bvq5AuvNkpWeJ7%2Fimage.png?alt=media&#x26;token=2eb6ddb6-ad7b-4546-b48c-9db489c4955d" alt=""><figcaption></figcaption></figure></div>

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

```tsx
const refreshResult = await baseQuery('/refreshToken', api, extraOptions)
```

Для того, чтобы ее правильно написать посмотри на [refresh token endpoint](https://api.flashcards.andrii.es/reference#tag/auth/post/v2/auth/refresh-token)

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

<pre class="language-tsx"><code class="lang-tsx">const release = await mutex.acquire()
<strong>      const refreshToken = localStorage.getItem('refreshToken')
</strong>
      try {
        const refreshResult = await baseQuery(
          {
<strong>            headers: {
</strong><strong>              Authorization: `Bearer ${refreshToken}`,
</strong><strong>            },
</strong><strong>            method: 'POST',
</strong><strong>            url: '/v2/auth/refresh-token',
</strong>          },
          api,
          extraOptions
        )
        
<strong>        console.log('refreshResult', refreshResult)
</strong></code></pre>

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

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

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

```tsx
const baseQuery = fetchBaseQuery({
  baseUrl: 'https://api.flashcards.andrii.es',
  prepareHeaders: headers => {
    const token = localStorage.getItem('accessToken')

    if (headers.get('Authorization')) {
      return headers
    }

    if (token) {
      headers.set('Authorization', `Bearer ${token}`)
    }

    return headers
  },
})
```

<figure><img src="https://3919421388-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjRpDZ5OOulcxs84wk3X1%2Fuploads%2FQZ4KOE78CCIcxCj5jZHQ%2Fimage.png?alt=media&#x26;token=b04c4296-0dec-49a8-a0f2-464228ff98ce" alt=""><figcaption></figcaption></figure>

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

<pre class="language-tsx"><code class="lang-tsx">try {
        const refreshResult = (await baseQuery(
          {
            headers: {
              Authorization: `Bearer ${refreshToken}`,
            },
            method: 'POST',
            url: '/v2/auth/refresh-token',
          },
          api,
          extraOptions
<strong>        )) as any
</strong>
        if (refreshResult.data) {
<strong>          localStorage.setItem('accessToken', refreshResult.data.accessToken)
</strong><strong>          localStorage.setItem('refreshToken', refreshResult.data.refreshToken)
</strong>
          // retry the initial query
<strong>          result = await baseQuery(args, api, extraOptions)
</strong>        } else {
<strong>          router.navigate('/login')
</strong>        }
      } finally {
        // release must be called once the mutex should be released again.
        release()
      }
</code></pre>

* С типизацией придется мучаться т.к. библиотека RTK query не возвращает нам `QueryReturnValue`
* чтобы сделать редирект на логин экспортируйте роутер
* Когда будете делать [logout](https://api.flashcards.andrii.es/reference#tag/auth/post/v2/auth/logout), зачищаейте localstorage

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