18 KiB
OAuth2 з паролем (і хешуванням), Bearer з токенами JWT
Тепер, коли ми маємо весь потік безпеки, зробімо застосунок справді захищеним, використовуючи токени JWT і безпечне хешування паролів.
Цей код ви можете реально використовувати у своєму застосунку, зберігати хеші паролів у своїй базі даних тощо.
Ми почнемо з того місця, де зупинилися в попередньому розділі, і розширимо його.
Про JWT
JWT означає «JSON Web Tokens».
Це стандарт кодування об'єкта JSON у довгий щільний рядок без пробілів. Він виглядає так:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Він не зашифрований, тому кожен може відновити інформацію з вмісту.
Але він підписаний. Тож коли ви отримуєте токен, який ви видали, ви можете перевірити, що справді його видали ви.
Таким чином, ви можете створити токен із терміном дії, наприклад, 1 тиждень. І коли користувач повернеться наступного дня з токеном, ви знатимете, що користувач досі увійшов у вашу систему.
Через тиждень токен завершить термін дії, і користувач не буде авторизований та має знову увійти, щоб отримати новий токен. І якщо користувач (або третя сторона) намагатиметься змінити токен, щоб змінити термін дії, ви це виявите, бо підписи не співпадатимуть.
Якщо хочете «погратися» з токенами JWT і побачити, як вони працюють, перегляньте https://jwt.io.
Встановіть PyJWT
Нам потрібно встановити PyJWT, щоб створювати та перевіряти токени JWT у Python.
Переконайтеся, що ви створили віртуальне оточення, активували його і тоді встановіть pyjwt:
$ pip install pyjwt
---> 100%
/// info | Інформація
Якщо ви плануєте використовувати алгоритми цифрового підпису на кшталт RSA або ECDSA, слід встановити залежність криптобібліотеки pyjwt[crypto].
Докладніше про це можна прочитати у документації з встановлення PyJWT.
///
Хешування паролів
«Хешування» означає перетворення деякого вмісту (у цьому випадку пароля) на послідовність байтів (просто строку), що виглядає як нісенітниця.
Кожного разу, коли ви передаєте точно той самий вміст (точно той самий пароль), ви отримуєте точно ту саму «нісенітницю».
Але не можна перетворити цю «нісенітницю» назад у пароль.
Навіщо використовувати хешування паролів
Якщо вашу базу даних вкрадуть, зловмисник не матиме відкритих паролів ваших користувачів, а лише хеші.
Тож зловмисник не зможе спробувати використати цей пароль в іншій системі (оскільки багато користувачів використовують той самий пароль всюди, це було б небезпечно).
Встановіть pwdlib
pwdlib - це чудовий пакет Python для роботи з хешами паролів.
Він підтримує багато безпечних алгоритмів хешування та утиліт для роботи з ними.
Рекомендований алгоритм - «Argon2».
Переконайтеся, що ви створили віртуальне оточення, активували його і тоді встановіть pwdlib з Argon2:
$ pip install "pwdlib[argon2]"
---> 100%
/// tip | Порада
З pwdlib ви навіть можете налаштувати його так, щоб він умів читати паролі, створені Django, плагіном безпеки Flask або багатьма іншими.
Тож ви зможете, наприклад, спільно використовувати ті самі дані з застосунку Django в базі даних із застосунком FastAPI. Або поступово мігрувати застосунок Django, використовуючи ту саму базу даних.
І ваші користувачі зможуть входити як із вашого застосунку Django, так і з вашого застосунку FastAPI одночасно.
///
Хешування і перевірка паролів
Імпортуйте потрібні інструменти з 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
Імпортуйте встановлені модулі.
Створіть випадковий секретний ключ, який буде використано для підписання токенів JWT.
Щоб згенерувати безпечний випадковий секретний ключ, використайте команду:
$ openssl rand -hex 32
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
І скопіюйте вивід у змінну SECRET_KEY (не використовуйте той, що в прикладі).
Створіть змінну ALGORITHM з алгоритмом для підписання токена JWT і встановіть її в "HS256".
Створіть змінну для терміну дії токена.
Визначте модель Pydantic, яка буде використана в кінцевій точці токена для відповіді.
Створіть утилітарну функцію для генерації нового токена доступу.
{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,82:90] *}
Оновіть залежності
Оновіть get_current_user, щоб отримувати той самий токен, що й раніше, але цього разу - токен JWT.
Декодуйте отриманий токен, перевірте його та поверніть поточного користувача.
Якщо токен недійсний, одразу поверніть помилку HTTP.
{* ../../docs_src/security/tutorial004_an_py310.py hl[93:110] *}
Оновіть операцію шляху /token
Створіть timedelta з часом життя токена.
Створіть справжній токен доступу JWT і поверніть його.
{* ../../docs_src/security/tutorial004_an_py310.py hl[121:136] *}
Технічні деталі про «subject» sub у JWT
Специфікація JWT каже, що існує ключ sub із суб'єктом токена.
Використовувати його не обов'язково, але саме туди зазвичай поміщають ідентифікатор користувача, тож ми використовуємо його тут.
JWT може використовуватися й для інших речей, окрім ідентифікації користувача та надання йому можливості безпосередньо виконувати операції з вашою API.
Наприклад, ви можете ідентифікувати «автомобіль» або «допис у блозі».
Тоді ви можете додати дозволи щодо цієї сутності, як-от «керувати» (для автомобіля) або «редагувати» (для допису).
І потім ви можете видати цей токен JWT користувачу (або боту), і він зможе виконувати ці дії (керувати автомобілем або редагувати допис), навіть не маючи облікового запису - лише з токеном JWT, який ваша API для цього згенерувала.
Використовуючи ці ідеї, JWT можна застосовувати у значно складніших сценаріях.
У таких випадках кілька сутностей можуть мати однакові ідентифікатори, скажімо foo (користувач foo, автомобіль foo і допис foo).
Щоб уникнути колізій ідентифікаторів, під час створення токена JWT для користувача ви можете додати префікс до значення ключа sub, наприклад username:. Отже, у цьому прикладі значення sub могло б бути: username:johndoe.
Важливо пам'ятати, що ключ sub має містити унікальний ідентифікатор у межах усього застосунку і має бути строкою.
Перевірте
Запустіть сервер і перейдіть до документації: http://127.0.0.1:8000/docs.
Ви побачите такий інтерфейс користувача:
Авторизуйте застосунок так само, як раніше.
Використайте облікові дані:
Username: johndoe
Password: secret
/// check | Перевірте
Зверніть увагу, що ніде в коді немає відкритого пароля "secret", ми маємо лише хешовану версію.
///
Викличте кінцеву точку /users/me/, ви отримаєте відповідь:
{
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"disabled": false
}
Якщо відкриєте інструменти розробника, ви побачите, що у відправлених даних є лише токен, пароль надсилається тільки в першому запиті для автентифікації користувача та отримання токена доступу, але не надсилається далі:
/// note | Примітка
Зверніть увагу на заголовок Authorization зі значенням, що починається з Bearer .
///
Просунуте використання зі scopes
OAuth2 має поняття «scopes».
Ви можете використовувати їх, щоб додати конкретний набір дозволів до токена JWT.
Потім ви можете видати цей токен користувачу напряму або третій стороні для взаємодії з вашою API із набором обмежень.
Ви можете дізнатися, як їх використовувати і як вони інтегровані з FastAPI пізніше у просунутому посібнику користувача.
Підсумок
Маючи все, що ви бачили досі, ви можете налаштувати захищений застосунок FastAPI, використовуючи стандарти на кшталт OAuth2 і JWT.
Майже в будь-якому фреймворку опрацювання безпеки дуже швидко стає досить складною темою.
Багато пакетів, що сильно це спрощують, змушені йти на численні компроміси з моделлю даних, базою даних і доступними можливостями. Дехто з цих пакетів, які надто все спрощують, насправді мають приховані вади безпеки.
FastAPI не йде на жодні компроміси з будь-якою базою даних, моделлю даних чи інструментом.
Він дає вам усю гнучкість, щоб обрати ті, які найкраще підходять вашому проєкту.
І ви можете напряму використовувати добре підтримувані та широко застосовувані пакети на кшталт pwdlib і PyJWT, адже FastAPI не вимагає жодних складних механізмів для інтеграції зовнішніх пакетів.
Водночас він надає інструменти, щоб максимально спростити процес без компромісів у гнучкості, надійності чи безпеці.
І ви можете використовувати та впроваджувати безпечні стандартні протоколи, як-от OAuth2, відносно простим способом.
У просунутому посібнику користувача ви можете дізнатися більше про те, як використовувати «scopes» в OAuth2 для більш детальної системи дозволів, дотримуючись тих самих стандартів. OAuth2 зі scopes - це механізм, який використовують багато великих провайдерів автентифікації, як-от Facebook, Google, GitHub, Microsoft, X (Twitter) тощо, щоб авторизувати сторонні застосунки на взаємодію з їхніми API від імені користувачів.