Модуль 30

OAuth2 і FastAPI Security

Ось твій урок у стилі CS50. Приготуйся, зараз будемо розбирати магію безпеки! 🚀


🛡️ Урок: OAuth2 та Безпека у FastAPI

(або "Як пустити в клуб тільки своїх і не перевіряти паспорт щоразу")


1. 🔥 Вступ: Проблема та Мотивація

Привіт, друзі! 👋

Уявіть, що ви заселилися у готель "All Inclusive". Ви підходите до бару за коктейлем. Бармен питає: "Ти хто?". Ви дістаєте паспорт, договір про бронювання, чек про оплату, показуєте це все. Він перевіряє 5 хвилин, наливає сік.

Через 10 хвилин ви хочете піти в басейн. Охоронець: "Ти хто?". Ви знову дістаєте стос паперів...

Звучить як пекло, правда?

Але ж у реальному житті це працює не так. Вам на руку чіпляють браслет. Цей браслет — це відповідь на питання "Чи можна цій людині бути тут?", без необхідності носити з собою сейф із документами.

У світі веб-розробки: * Паспорт і документи — це ваш логін і пароль. Пересилати їх з кожним запитом (GET /me, POST /orders) — це небезпечно і незручно. * Браслет — це Token (Токен).

Сьогодні ми навчимося створювати ці "браслети" за допомогою стандарту OAuth2 у FastAPI.

Без цього уроку ваш API — це прохідний двір, де будь-хто може видалити вашу базу даних. Готові закрити двері на замок? Поїхали!


2. 🧠 Теоретична база (без нудних лекцій)

Перш ніж писати код, розберемося з механікою.

🔑 Автентифікація vs Авторизація

Це плутають навіть мідли. Запам’ятайте раз і назавжди: 1. Автентифікація (Authentication): "Хто ти?" (Перевірка логіна/пароля). 2. Авторизація (Authorization): "Що тобі можна?" (Чи є в тебе права адміна?).

Сьогодні ми фокусуємось на першому, яке відкриває двері до другого.

📜 Що таке OAuth2?

Це не бібліотека, це протокол. Це набір правил, як домовлятися про доступ. У контексті FastAPI ми найчастіше використовуємо схему "Password Flow": 1. Клієнт відправляє username + password. 2. Сервер каже: "Ок, вірю" і віддає Token. 3. Клієнт у всіх наступних запитах показує тільки Token.

📦 Що таке JWT (JSON Web Token)?

Це формат нашого "браслета". Це не просто набір випадкових символів. Це зашифрований JSON, який містить інформацію про користувача.

Уявіть прозорий контейнер, у якому лежить записка: "Це Іван, він адмін, токен дійсний до 12:00". * Під капотом: Цей контейнер підписаний цифровим підписом (секретним ключем сервера). * Чому це круто: Якщо хакер спробує змінити "Іван" на "Хакер", підпис зламається, і сервер не прийме токен.

❗️ Що треба запам'ятати: Ми ніколи не зберігаємо паролі у відкритому вигляді. Ми хешуємо їх (перетворюємо на "фарш", з якого не відновити "м'ясо"). Але про хешування — трохи згодом, зараз фокус на токенах.


3. 🧪 Приклади: Від простого до магії

Нам знадобляться бібліотеки. Уявіть, що це наші інструменти для виготовлення перепусток.

pip install fastapi uvicorn python-multipart python-jose[cryptography] passlib[bcrypt]

(Не лякайтесь назв: multipart — для форм, jose — для роботи з JWT, passlib — для хешування паролів).

Крок 1: Найпростіша перевірка (Dummy Example)

Давайте зробимо "фейкову" базу даних і просто навчимо FastAPI розуміти, що ми хочемо використовувати OAuth2.

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

app = FastAPI()

# 1. Схема безпеки. Ми кажемо Swagger UI: "Ей, логін відбувається за адресою /token"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    # Тут магія: FastAPI автоматично шукає заголовок "Authorization: Bearer ..."
    # Якщо токена немає — він сам викине помилку 401.
    return {"token": token, "data": "Дуже секретні дані"}

🤔 Питання до вас: Якщо ви зараз запустите цей код і спробуєте зробити запит на /items/ без заголовків, що станеться? (Відповідь: Ви отримаєте "Not authenticated". FastAPI зробив це за нас!)


Крок 2: Реальна генерація токена (The Real Deal)

Тепер зробимо ендпоінт, який дійсно перевіряє пароль і видає токен.

# Додамо трохи імітації бази даних
fake_users_db = {
    "david": {"username": "david", "password": "secretpassword"},
    "alice": {"username": "alice", "password": "wonderland"},
}

@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    # OAuth2PasswordRequestForm — це спеціальний клас, який очікує поля 'username' та 'password'

    user = fake_users_db.get(form_data.username)

    # Перевірка: чи існує юзер і чи правильний пароль
    if not user or user["password"] != form_data.password:
        raise HTTPException(status_code=400, detail="Невірний логін або пароль")

    # Якщо все ок — видаємо токен (поки що просто рядок, щоб зрозуміти суть)
    return {"access_token": user["username"], "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(token: str = Depends(oauth2_scheme)):
    # Тепер ми розшифровуємо токен (у нашому простому випадку — це просто ім'я)
    user = fake_users_db.get(token)
    if not user:
         raise HTTPException(status_code=401, detail="Токен недійсний")
    return user

Що ми зробили? 1. Створили /token, куди користувач шле логін/пароль. 2. Якщо вони вірні — віддаємо JSON з токеном. 3. Створили /users/me, який захищений. Він бере токен, дивиться на нього і розуміє, хто прийшов.


Крок 3: Справжній JWT (Рівень PRO) 🛡️

Просто віддавати ім'я як токен — це як написати "Перепустка" на серветці. Легко підробити. Використаємо криптографію.

from datetime import datetime, timedelta
from jose import JWTError, jwt

# СЕКРЕТНИЙ КЛЮЧ (У реальному житті це має бути в .env файлі!)
SECRET_KEY = "my_super_secret_key_shhh"
ALGORITHM = "HS256"

def create_access_token(data: dict):
    to_encode = data.copy()
    # Токен живе 30 хвилин
    expire = datetime.utcnow() + timedelta(minutes=30)
    to_encode.update({"exp": expire})

    # Створюємо підписаний JWT
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# Оновлений логін
@app.post("/token")
async def login_jwt(form_data: OAuth2PasswordRequestForm = Depends()):
    user = fake_users_db.get(form_data.username)
    if not user or user["password"] != form_data.password:
        raise HTTPException(status_code=400, detail="Error")

    # ГЕНЕРУЄМО JWT
    access_token = create_access_token(data={"sub": user["username"]})
    return {"access_token": access_token, "token_type": "bearer"}

# Оновлена перевірка (Dependency)
async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        # Спроба розшифрувати
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except JWTError:
        raise HTTPException(status_code=401, detail="Could not validate credentials")

    user = fake_users_db.get(username)
    return user

@app.get("/users/me")
async def read_users_me(current_user: dict = Depends(get_current_user)):
    return current_user

Як це працює? Коли клієнт присилає токен, ми намагаємось відкрити його нашим SECRET_KEY. * Якщо токен підробив хакер — ключ не підійде ❌. * Якщо час вийшов — бібліотека викине помилку ❌. * Якщо все ок — ми дістаємо ім'я юзера ✅.


4. 🛠 Практична частина

Час закатати рукави! Ваше завдання не просто скопіювати, а відчути код.

  1. 🔹 Завдання "Hello World": Скопіюйте код з Кроку 3. Запустіть сервер (uvicorn main:app --reload). Відкрийте Swagger UI (/docs). Натисніть зелену кнопку Authorize, введіть david / secretpassword. Перевірте, чи пускає вас на /users/me.
  2. 🔹 Завдання "Новий гість": Додайте в fake_users_db себе (своє ім'я і пароль). Спробуйте залогінитися.
  3. 🔹 Завдання "Фейс-контроль": Змініть час життя токена на 10 секунд. Залогіньтесь, швидко зробіть запит (успіх), почекайте 11 секунд і зробіть запит знову. Що ви побачили?
  4. 🔹 Завдання "Злам системи": Отримайте токен. Скопіюйте його. Підіть на сайт jwt.io, вставте токен. Змініть у ньому "sub": "david" на "sub": "alice". Спробуйте використати цей підроблений токен у Swagger. (Спойлер: має бути помилка, бо підпис не зійдеться).
  5. 🔹 Міні-кейс: Додайте до ендпоінту /users/me повернення повідомлення: "Привіт, [username], ласкаво просимо!".

5. 💡 Мислення як у розробника

Як думає новачок?

"Я зроблю так, щоб воно просто працювало. Зберігати паролі в Excel? Та хто там побачить!" 😨

Як думає Senior Engineer?

"Безпека — це шари. Ніколи не довіряй вхідним даним. Ніколи не пиши свою криптографію." 🧐

Типові помилки: 1. Зберігання SECRET_KEY в коді. Якщо ви запушите це на GitHub, вважайте, що вас зламали. Використовуйте змінні середовища (.env). 2. Зберігання паролів текстом. У нашому прикладі ми так робили для простоти. У реальності — тільки хеш (використовуйте bcrypt). 3. Винахід велосипеда. Не пишіть свою функцію генерації токенів. Бібліотеки писали математики, використовуйте їх.


6. 🧩 Підсумок

Сьогодні ми пройшли шлях від "хто ти такий?" до сучасної системи безпеки на основі JWT.

Що ви тепер вмієте: * ✅ Розуміти різницю між AuthN та AuthZ. * ✅ Створювати механізм входу (Login) у FastAPI. * ✅ Генерувати захищені токени. * ✅ Закривати ендпоінти від неавторизованих користувачів.

Інтрига на наступний урок: Зараз наші юзери живуть у словнику в пам'яті Python. Якщо перезавантажити сервер — вони зникнуть. Як зберегти їх назавжди? Наступного разу ми підключимо справжню базу даних SQL і навчимося "солити" паролі! 🧂

Це був CS50... тобто, це був ваш урок з FastAPI Security. Успіхів у коді!