Вход и JWT access token
Уроки курсаВход и JWT access token
POST /api/auth/login: verify перед выдачей access_token
Добавьте два новых файла и замените main.py.
app/schemas/auth.py — входной и выходной контракт эндпоинта:
from pydantic import BaseModel, EmailStr, Field
class LoginRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=128)
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
app/routers/auth.py — сам маршрут:
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User
from app.passwords import pwd_context
from app.schemas.auth import LoginRequest, TokenResponse
from app.settings import Settings, get_settings
from app.tokens import create_access_token
router = APIRouter()
@router.post("/login", response_model=TokenResponse)
def login(data: LoginRequest, db: Session = Depends(get_db), settings: Settings = Depends(get_settings)):
user = db.scalars(select(User).where(User.email == str(data.email))).first()
if user is None or not pwd_context.verify(data.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"})
return {"access_token": create_access_token(user.token_subject, settings.secret_key), "token_type": "bearer"}
Порядок операций принципиален. Сначала ищем пользователя по email. Если пользователь не найден или pwd_context.verify вернул False — возвращаем один и тот же ответ: 401 с detail="Invalid email or password". Разный текст для «нет такого email» и «неверный пароль» подсказал бы атакующему, какие адреса зарегистрированы. Одинаковый JSON этот канал частично закрывает, но не устраняет разницу по времени ответа: verify у Argon2 занимает заметно больше времени, чем простой SELECT. Полная защита от перечисления email — отдельная задача, выходящая за пределы этого урока.
Заголовок WWW-Authenticate: Bearer — требование спецификации OAuth 2.0 для ответов на защищённые ресурсы.
Токен выдаётся только после успешного verify. Пароль и хеш в payload не попадают — только token_subject, который уже существует в модели User.
app/main.py с подключением нового роутера:
from fastapi import FastAPI, Depends
from app.routers import rooms, bookings, users, auth
from app.settings import Settings, get_settings
app = FastAPI()
app.include_router(rooms.router, prefix="/api/rooms")
app.include_router(bookings.router, prefix="/api/rooms")
app.include_router(users.router, prefix="/api/users")
app.include_router(auth.router, prefix="/api/auth")
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {"app": settings.app_name}
Создайте test_login.py в корне auth_demo. Сохраните conftest.py и прежние тесты без изменений:
from app.passwords import pwd_context
from app.models import User
from app.database import get_db
def test_login_success_and_rejections(client):
body = {"email": "owner@example.com", "password": "student-pass-123"}
registered = client.post("/api/users", json=body)
assert registered.status_code == 201
response = client.post("/api/auth/login", json=body)
assert response.status_code == 200
assert set(response.json()) == {"access_token", "token_type"}
assert response.json()["token_type"] == "bearer"
assert response.json()["access_token"].count(".") == 2
for email, password in [(body["email"], "other-pass-123"), ("missing@example.com", body["password"])]:
rejected = client.post("/api/auth/login", json={"email": email, "password": password})
assert rejected.status_code == 401
assert rejected.json() == {"detail": "Invalid email or password"}
Тест регистрирует пользователя, проверяет успешный вход и убеждается, что оба вида отказа — неверный пароль и несуществующий email — дают идентичный 401. Проверка count(".") == 2 подтверждает только наличие двух разделителей. Она не проверяет содержимое токена или подпись. Запустите все тесты из корня auth_demo:
python -m pytest
Выдача токена — не защита маршрутов. Маршруты /api/rooms и /api/rooms/{room_id}/bookings пока не требуют токен и не проверяют его.
