# OAuth2 с паролем (и хешированием), Bearer с JWT-токенами { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens }

Теперь, когда у нас определен процесс обеспечения безопасности, давайте сделаем приложение действительно безопасным, используя токены <abbr title="JSON Web Tokens - веб‑токены JSON">JWT</abbr> и безопасное хеширование паролей.

Этот код можно реально использовать в своем приложении, сохранять хэши паролей в базе данных и т.д.

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

## Про JWT { #about-jwt }

JWT означает "JSON Web Tokens".

Это стандарт для кодирования JSON-объекта в виде длинной строки без пробелов. Выглядит это следующим образом:

```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```

Он не зашифрован, поэтому любой человек может восстановить информацию из его содержимого.

Но он подписан. Следовательно, когда вы получаете токен, который вы эмитировали (выдавали), вы можете убедиться, что это именно вы его эмитировали.

Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизован в вашей системе.

Через неделю срок действия токена истечет, пользователь не будет авторизован и ему придется заново входить в систему, чтобы получить новый токен. А если пользователь (или третье лицо) попытается модифицировать токен, чтобы изменить срок действия, вы сможете это обнаружить, поскольку подписи не будут совпадать.

Если вы хотите поиграть с JWT-токенами и посмотреть, как они работают, посмотрите [https://jwt.io](https://jwt.io/).

## Установка `PyJWT` { #install-pyjwt }

Нам необходимо установить `pyjwt` для генерации и проверки JWT-токенов на языке Python.

Убедитесь, что вы создали [виртуальное окружение](../../virtual-environments.md), активируйте его, а затем установите `pyjwt`:

<div class="termy">

```console
$ pip install pyjwt

---> 100%
```

</div>

/// info | Дополнительная информация

Если вы планируете использовать алгоритмы цифровой подписи, такие как RSA или ECDSA, вам следует установить зависимость библиотеки криптографии `pyjwt[crypto]`.

Подробнее об этом можно прочитать в [документации по установке PyJWT](https://pyjwt.readthedocs.io/en/latest/installation.html).

///

## Хеширование паролей { #password-hashing }

"Хеширование" означает преобразование некоторого содержимого (в данном случае пароля) в последовательность байтов (просто строку), которая выглядит как тарабарщина.

Каждый раз, когда вы передаете точно такое же содержимое (точно такой же пароль), вы получаете точно такую же тарабарщину.

Но преобразовать тарабарщину обратно в пароль невозможно.

### Для чего нужно хеширование паролей { #why-use-password-hashing }

Если ваша база данных будет украдена, то вор не получит пароли пользователей в открытом виде, а только их хэши.

Таким образом, вор не сможет использовать этот пароль в другой системе (поскольку многие пользователи везде используют один и тот же пароль, это было бы опасно).

## Установка `pwdlib` { #install-pwdlib }

pwdlib — это отличный пакет Python для работы с хэшами паролей.

Он поддерживает множество безопасных алгоритмов хеширования и утилит для работы с ними.

Рекомендуемый алгоритм — "Argon2".

Убедитесь, что вы создали [виртуальное окружение](../../virtual-environments.md), активируйте его, и затем установите pwdlib вместе с Argon2:

<div class="termy">

```console
$ pip install "pwdlib[argon2]"

---> 100%
```

</div>

/// tip | Подсказка

С помощью `pwdlib` можно даже настроить его на чтение паролей, созданных **Django**, плагином безопасности **Flask** или многими другими библиотеками.

Таким образом, вы сможете, например, совместно использовать одни и те же данные из приложения Django в базе данных с приложением FastAPI. Или постепенно мигрировать Django-приложение, используя ту же базу данных.

При этом пользователи смогут одновременно входить в систему как из приложения Django, так и из приложения **FastAPI**.

///

## Хеширование и проверка паролей { #hash-and-verify-the-passwords }

Импортируйте необходимые инструменты из `pwdlib`.

Создайте экземпляр PasswordHash с рекомендованными настройками — он будет использоваться для хэширования и проверки паролей.

/// tip | Подсказка

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

Например, вы можете использовать ее для чтения и проверки паролей, сгенерированных другой системой (например, Django), но хэшировать все новые пароли другим алгоритмом, например Argon2 или Bcrypt.

И при этом быть совместимым со всеми этими системами.

///

Создайте служебную функцию для хэширования пароля, поступающего от пользователя.

А затем создайте другую — для проверки соответствия полученного пароля и хранимого хэша.

И еще одну — для аутентификации и возврата пользователя.

{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,51,58:59,62:63,72:79] *}

Когда `authenticate_user` вызывается с именем пользователя, которого нет в базе данных, мы все равно запускаем `verify_password` с использованием фиктивного хэша.

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

/// note | Технические детали

Если проверить новую (фальшивую) базу данных `fake_users_db`, то можно увидеть, как теперь выглядит хэшированный пароль: `"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`.

///

## Работа с JWT токенами { #handle-jwt-tokens }

Импортируйте установленные модули.

Создайте случайный секретный ключ, который будет использоваться для подписи JWT-токенов.

Для генерации безопасного случайного секретного ключа используйте команду:

<div class="termy">

```console
$ openssl rand -hex 32

09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
```

</div>

И скопируйте полученный результат в переменную `SECRET_KEY` (не используйте тот, что в примере).

Создайте переменную `ALGORITHM` с алгоритмом, используемым для подписи JWT-токена, и установите для нее значение `"HS256"`.

Создайте переменную для срока действия токена.

Определите Pydantic-модель, которая будет использоваться для формирования ответа на запрос на получение токена.

Создайте служебную функцию для генерации нового токена доступа.

{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,82:90] *}

## Обновление зависимостей { #update-the-dependencies }

Обновите `get_current_user` для получения того же токена, что и раньше, но на этот раз с использованием JWT-токенов.

Декодируйте полученный токен, проверьте его и верните текущего пользователя.

Если токен недействителен, то сразу же верните HTTP-ошибку.

{* ../../docs_src/security/tutorial004_an_py310.py hl[93:110] *}

## Обновление *операции пути* `/token` { #update-the-token-path-operation }

Создайте `timedelta` со временем истечения срока действия токена.

Создайте реальный токен доступа JWT и верните его

{* ../../docs_src/security/tutorial004_an_py310.py hl[121:136] *}

### Технические подробности о JWT ключе `sub` { #technical-details-about-the-jwt-subject-sub }

В спецификации JWT говорится, что существует ключ `sub`, содержащий субъект токена.

Его использование необязательно, но это именно то место, куда вы должны поместить идентификатор пользователя, и поэтому мы здесь его и используем.

JWT может использоваться и для других целей, помимо идентификации пользователя и предоставления ему возможности выполнять операции непосредственно в вашем API.

Например, вы могли бы определить "автомобиль" или "запись в блоге".

Затем вы могли бы добавить права доступа к этой сущности, например "управлять" (для автомобиля) или "редактировать" (для блога).

Затем вы могли бы передать этот JWT-токен пользователю (или боту), и они использовали бы его для выполнения определенных действий (управление автомобилем или редактирование запись в блоге), даже не имея учетной записи, просто используя JWT-токен, сгенерированный вашим API.

Используя эти идеи, JWT можно применять для гораздо более сложных сценариев.

В отдельных случаях несколько сущностей могут иметь один и тот же идентификатор, скажем, `foo` (пользователь `foo`, автомобиль `foo` и запись в блоге `foo`).

Поэтому, чтобы избежать коллизий идентификаторов, при создании JWT-токена для пользователя можно добавить префикс `username` к значению ключа `sub`. Таким образом, в данном примере значение `sub` было бы `username:johndoe`.

Важно помнить, что ключ `sub` должен иметь уникальный идентификатор для всего приложения и представлять собой строку.

## Проверка в действии { #check-it }

Запустите сервер и перейдите к документации: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).

Вы увидите пользовательский интерфейс вида:

<img src="/img/tutorial/security/image07.png">

Пройдите авторизацию так же, как делали раньше.

Используя учетные данные пользователя:

Username: `johndoe`
Password: `secret`

/// check | Проверка

Обратите внимание, что нигде в коде не используется открытый текст пароля "`secret`", мы используем только его хэшированную версию.

///

<img src="/img/tutorial/security/image08.png">

Вызвав эндпоинт `/users/me/`, вы получите ответ в виде:

```JSON
{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false
}
```

<img src="/img/tutorial/security/image09.png">

Если открыть инструменты разработчика, то можно увидеть, что передаваемые данные включают только токен, пароль передается только в первом запросе для аутентификации пользователя и получения токена доступа, но не в последующих:

<img src="/img/tutorial/security/image10.png">

/// note | Техническая информация

Обратите внимание на HTTP-заголовок `Authorization`, значение которого начинается с `Bearer `.

///

## Продвинутое использование `scopes` { #advanced-usage-with-scopes }

В OAuth2 существует понятие "диапазоны" ("`scopes`").

С их помощью можно добавить определенный набор разрешений к JWT-токену.

Затем вы можете передать этот токен непосредственно пользователю или третьей стороне для взаимодействия с вашим API с определенным набором ограничений.

О том, как их использовать и как они интегрированы в **FastAPI**, читайте далее в **Расширенном руководстве пользователя**.

## Резюме { #recap }

С учетом того, что вы видели до сих пор, вы можете создать безопасное приложение **FastAPI**, используя такие стандарты, как OAuth2 и JWT.

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

Многие пакеты, сильно упрощающие эту задачу, вынуждены идти на многочисленные компромиссы с моделью данных, с базой данных и с доступным функционалом. Некоторые из этих пакетов, которые пытаются уж слишком все упростить, имеют даже "дыры" в системе безопасности.

---

**FastAPI** не делает уступок ни одной базе данных, модели данных или инструментарию.

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

Вы можете напрямую использовать многие хорошо поддерживаемые и широко распространенные пакеты, такие как `pwdlib` и `PyJWT`, поскольку **FastAPI** не требует сложных механизмов для интеграции внешних пакетов.

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

При этом вы можете использовать и реализовывать безопасные стандартные протоколы, такие как OAuth2, относительно простым способом.

В **Расширенном руководстве пользователя** вы можете узнать больше о том, как использовать "диапазоны" ("`scopes`") OAuth2 для создания более точно настроенной системы разрешений в соответствии с теми же стандартами. OAuth2 с диапазонами — это механизм, используемый многими крупными провайдерами сервиса аутентификации, такими как Facebook, Google, GitHub, Microsoft, X (Twitter) и др., для авторизации сторонних приложений на взаимодействие с их API от имени их пользователей.
