21 KiB
Аутентификация и авторизация
Эндпоинты для регистрации, верификации email, входа и управления JWT-токенами.
Быстрый старт
# 1. Регистрация
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","name_user":"John","password":"securepass123"}'
# 2. Проверить письмо, скопировать токен из логов (ConsoleEmailBackend)
# 3. Подтвердить email
curl -X POST http://localhost:8000/api/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{"token":"ТОКЕН_ИЗ_ПИСЬМА"}'
# 4. Войти и получить access-токен
curl -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=user@example.com&password=securepass123' \
-i # -i чтобы увидеть Set-Cookie с refresh-токеном
# 5. Использовать access-токен
curl http://localhost:8000/api/v1/users/me \
-H "Authorization: Bearer ТВОЙ_ACCESS_TOKEN"
# 6. Обновить access-токен (refresh-токен в cookie автоматический)
curl -X POST http://localhost:8000/api/v1/auth/refresh \
-b "refresh_token=ТВОЙ_REFRESH_TOKEN" \
-i
# 7. Выход (отозвать refresh-токен)
curl -X POST http://localhost:8000/api/v1/auth/logout \
-b "refresh_token=ТВОЙ_REFRESH_TOKEN" \
-i
Эндпоинты
GET /api/v1/auth/registration-options
Публичные опции карточки регистрации (без авторизации) — доступен ли выбор команды и список команд для селектора.
Ответ (200 OK):
{
"team_choice_enabled": true,
"teams": [
{"id": "550e8400-e29b-41d4-a716-446655440000", "name": "Alpha"},
{"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "name": "Backend"}
],
"email_domain": "example.com"
}
Поля:
team_choice_enabled— значение настройки инстансаregistration_team_choice(админка,PUT /api/v1/admin/settings); дефолтfalseteams— список команд, отсортированный по названию; пустой массив, еслиteam_choice_enabled=false(справочник команд не раскрывается, пока выбор выключен)email_domain— эталонный домен email при включённой настройке инстансаregistration_email_domain_enabled, иначеnull
POST /api/v1/auth/register
Зарегистрировать нового пользователя и отправить письмо для подтверждения email.
Тело запроса:
{
"email": "user@example.com",
"name_user": "Иван Петров",
"password": "securepass123",
"team_id": "550e8400-e29b-41d4-a716-446655440000"
}
Поля:
email(строка, email): Адрес электронной почты; уникален в системеname_user(строка, 1-255 символов): Отображаемое имя пользователяpassword(строка, минимум 8 символов): Пароль (хэшируется с argon2)team_id(UUID, опционально): Команда пользователя; допустим только когдаregistration_team_choiceвключена в настройках инстанса (см.GET /registration-options) иteam_idссылается на существующую команду
Ответ (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name_user": "Иван Петров",
"role": "user"
}
Ошибки:
409 Conflict(email_already_registered) — пользователь с таким email уже зарегистрирован400 Bad Request(invalid_team_selection) — переданteam_id, а выбор команды выключен в настройках, либо команда с таким id не существует (единая ошибка для обоих случаев — публичный эндпоинт не перебирает id команд)400 Bad Request(invalid_email_domain) — включена верификация домена email (registration_email_domain_enabled), а домен вemail(часть после@, без учёта регистра) не совпадает с эталоннымregistration_email_domain; проверяется до создания пользователя422 Unprocessable Entity— валидация (пароль < 8 символов, некорректный email и т.д.)
Побочный эффект:
- На указанный email отправляется письмо с ссылкой на верификацию (консоль в dev)
POST /api/v1/auth/verify-email
Подтвердить email пользователя по одноразовому токену из письма.
Тело запроса:
{
"token": "ДЛИННЫЙ_ТОКЕН_ИЗ_ПИСЬМА"
}
Поля:
token(строка): Одноразовый токен подтверждения (256 бит, URL-safe base64)
Ответ (204 No Content): — пустой ответ при успехе
Ошибки:
400 Bad Request(invalid_or_expired_token) — токен не найден, уже использован или истёк (TTL по.env:EMAIL_VERIFICATION_TTL_HOURS, по умолчанию 24 часа)
Побочный эффект:
- Устанавливает
email_verified = trueдля пользователя - Отмечает токен как использованный (
used_at = now)
POST /api/v1/auth/token
OAuth2 password flow: аутентификация по email и паролю. Выдаёт пару токенов (access + refresh).
Тело запроса: (form-data или application/x-www-form-urlencoded)
username=user@example.com&password=securepass123
Параметры:
username(строка): Email пользователя (OAuth2 соглашение используетusername)password(строка): Пароль в открытом виде
Ответ (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Поля:
access_token(строка): JWT access-токен; используется в заголовкеAuthorization: Bearer <token>token_type(строка): Всегда"bearer"
Заголовок ответа:
Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000
Ошибки:
401 Unauthorized(invalid_credentials) — email не найден или пароль неверный403 Forbidden(email_not_verified) — email ещё не подтвержден (требуется/verify-email)
Описание токенов:
| Параметр | Тип | TTL | Место | Ротация |
|---|---|---|---|---|
| access_token | JWT | 15 минут | Тело ответа | Не ротируется; истекает автоматически |
| refresh_token | JWT | 30 дней | httpOnly cookie | Ротируется при каждом использовании /refresh |
Содержимое access-токена (payload):
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"role": "user",
"type": "access",
"exp": 1234567890,
"iat": 1234567200
}
Содержимое refresh-токена (payload):
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"jti": "UNIQUE_ID",
"type": "refresh",
"exp": 1234567890,
"iat": 1234567200
}
POST /api/v1/auth/refresh
Ротировать refresh-токен и выдать новый access-токен.
Параметры:
- Cookie:
refresh_token=ТВОЙ_REFRESH_TOKEN(httpOnly, отправляется браузером автоматически)
Ответ (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Заголовок ответа:
Set-Cookie: refresh_token=NEW_JWT; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000
Ошибки:
401 Unauthorized(missing_refresh_token) — cookierefresh_tokenне передана401 Unauthorized(invalid_refresh_token) — токен невалиден, просрочен, отозван или уже был использован (reuse)
Механика ротации:
- Backend декодирует refresh-токен и проверяет его
jtiв Redis (refresh:{jti}→user_id) - Если
jtiесть в Redis → токен валиден, пользователь существует - Если
jtiотсутствует → reuse или отозван →401 - Старый
jtiнемедленно удаляется из Redis - Выдаётся новый refresh-токен с новым
jti, добавляется в Redis - Повторное использование уже потраченного refresh-токена автоматически блокируется
POST /api/v1/auth/logout
Отозвать refresh-токен (удалить его из Redis) и погасить httpOnly cookie.
Параметры:
- Cookie:
refresh_token=ТВОЙ_REFRESH_TOKEN(опционально; если отсутствует, просто удалится cookie)
Ответ (204 No Content): — пустой ответ при успехе
Побочный эффект:
- Удаляет
jtiиз Redis → последующие попытки использовать этот refresh-токен дадут401 - Удаляет cookie
refresh_token(устанавливает в пустое значение)
GET /api/v1/users/me
Получить профиль текущего аутентифицированного пользователя.
Параметры:
- Заголовок:
Authorization: Bearer <access_token>(обязателен)
Ответ (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name_user": "Иван Петров",
"role": "user"
}
Ошибки:
401 Unauthorized(not_authenticated) — access-токен отсутствует, невалиден или истёк
RBAC (Role-Based Access Control)
Роли
admin— администратор (полный доступ, требуется require_admin)user— обычный пользователь (доступ к основным функциям)guest— не аутентифицированный пользователь (отсутствие JWT)
Зависимости FastAPI для авторизации
get_current_user — требуется аутентификация
@router.get("/profile")
async def profile(user: Annotated[User, Depends(get_current_user)]) -> UserOut:
"""Доступно только аутентифицированным пользователям."""
return user
- Возвращает объект User
- 401 если токен отсутствует, невалиден или истёк
get_current_user_optional — опциональная аутентификация (guest = None)
@router.get("/public")
async def public_data(user: Annotated[User | None, Depends(get_current_user_optional)]) -> dict:
"""Доступно всем; guest может прочитать, но не будет знать о себе."""
if user is None:
return {"message": "hello guest"}
return {"message": f"hello {user.name_user}"}
- Возвращает User или None
- Никогда не выбрасывает 401; отсутствие JWT →
None - Важно: Guest в системе — это отсутствие JWT, а не отдельное enum-значение в БД
require_admin — требуется роль admin
@router.delete("/users/{id}")
async def delete_user(id: uuid.UUID, admin: Annotated[User, Depends(require_admin)]) -> None:
"""Доступно только администраторам."""
# ...
- 401 если не аутентифицирован
- 403 если роль не
admin
Архитектурные решения
Refresh-токены в httpOnly cookies
Refresh-токены хранятся в httpOnly cookie (недоступно из JavaScript), чтобы защитить их от XSS. Браузер отправляет cookie автоматически на каждый запрос к /api/v1/auth/refresh.
Почему httpOnly + Secure + SameSite=Strict:
- httpOnly — недоступно из JavaScript (защита от XSS)
- Secure — передаётся только по HTTPS (защита от MITM)
- SameSite=Strict — не отправляется при кросс-сайтовых запросах (защита от CSRF)
Server-side refresh-token validation
Refresh-токены проверяются через Redis-хранилище (refresh:{jti} → user_id):
- Отзыв: удаление из Redis на logout или reuse
- Ротация: старый jti удаляется немедленно после использования → reuse → 401
- TTL: Redis-ключ имеет TTL, равный сроку жизни токена
Это позволяет:
- Отозвать токены без пересоздания ключей подписи
- Обнаружить replay-атаки (повторное использование старого токена)
- Контролировать количество активных сессий per user (если реализовать)
Сравнение с альтернативами
| Подход | Плюсы | Минусы | Используется |
|---|---|---|---|
| httpOnly cookie | Защита от XSS | Требует SameSite для защиты CSRF | ✅ Refresh-токен |
| Bearer token в теле | Явный контроль | Уязвимо для XSS | ✅ Access-токен (одноразовый) |
| Opaque tokens (session ID) | Компактно | Требует БД на каждый запрос | ❌ Не используется |
Примеры использования
JavaScript / TypeScript (frontend)
// Регистрация
async function register(email: string, name: string, password: string) {
const res = await fetch('/api/v1/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name_user: name, password }),
})
if (!res.ok) throw new Error(`Error: ${res.status}`)
return await res.json()
}
// Подтверждение email
async function verifyEmail(token: string) {
const res = await fetch('/api/v1/auth/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
if (!res.ok) throw new Error(`Error: ${res.status}`)
}
// Вход
async function login(email: string, password: string) {
const formData = new URLSearchParams()
formData.append('username', email)
formData.append('password', password)
const res = await fetch('/api/v1/auth/token', {
method: 'POST',
body: formData,
credentials: 'include', // ВАЖНО: отправлять cookies
})
if (!res.ok) throw new Error(`Error: ${res.status}`)
const data = await res.json()
localStorage.setItem('access_token', data.access_token) // Храним access-токен
// refresh-токен в cookie автоматический (httpOnly, браузер управляет)
return data
}
// Использование access-токена
async function getProfile() {
const token = localStorage.getItem('access_token')
const res = await fetch('/api/v1/users/me', {
headers: { 'Authorization': `Bearer ${token}` },
})
if (!res.ok) throw new Error(`Error: ${res.status}`)
return await res.json()
}
// Обновление access-токена (refresh-токен в cookie отправляется автоматически)
async function refreshToken() {
const res = await fetch('/api/v1/auth/refresh', {
method: 'POST',
credentials: 'include', // ВАЖНО: отправлять cookies
})
if (!res.ok) throw new Error(`Error: ${res.status}`)
const data = await res.json()
localStorage.setItem('access_token', data.access_token)
return data
}
// Выход
async function logout() {
const res = await fetch('/api/v1/auth/logout', {
method: 'POST',
credentials: 'include', // ВАЖНО: отправлять cookies
})
localStorage.removeItem('access_token') // Удаляем access-токен
// refresh-токен будет удалён серверноvim (cookie)
}
curl примеры
# Полный цикл регистрации и входа
# 1. Регистрация
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","name_user":"Alice","password":"securepass123"}'
# Ответ: {"id":"...", "email":"alice@example.com", "name_user":"Alice", "role":"user"}
# 2. Проверить логи консоли (ConsoleEmailBackend выведет токен подтверждения)
# Из вывода скопировать token, например: "...token=abc123..."
# 3. Подтвердить email
curl -X POST http://localhost:8000/api/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{"token":"abc123"}'
# Ответ: (204 No Content)
# 4. Вход
curl -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=alice@example.com&password=securepass123' \
-i
# Ответ:
# HTTP/1.1 200 OK
# Set-Cookie: refresh_token=eyJ...; HttpOnly; Secure; SameSite=Strict; ...
# {"access_token":"eyJ...", "token_type":"bearer"}
# 5. Сохранить access-токен из ответа, использовать его
export ACCESS_TOKEN="eyJ..."
# 6. Получить профиль
curl http://localhost:8000/api/v1/users/me \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Ответ: {"id":"...", "email":"alice@example.com", "name_user":"Alice", "role":"user"}
# 7. Обновить access-токен (refresh_token в cookie отправляется автоматически)
curl -X POST http://localhost:8000/api/v1/auth/refresh \
-b "refresh_token=eyJ..." \
-i
# Ответ:
# HTTP/1.1 200 OK
# Set-Cookie: refresh_token=NEW_TOKEN; HttpOnly; Secure; SameSite=Strict; ...
# {"access_token":"NEW_ACCESS_TOKEN", "token_type":"bearer"}
# 8. Выход
curl -X POST http://localhost:8000/api/v1/auth/logout \
-b "refresh_token=eyJ..." \
-i
# Ответ: HTTP/1.1 204 No Content (cookie удалена)
# Попытка использовать refresh-токен после logout → 401
curl -X POST http://localhost:8000/api/v1/auth/refresh \
-b "refresh_token=eyJ..." \
-i
# Ответ: HTTP/1.1 401 Unauthorized {"detail":"invalid_refresh_token"}
Константы и конфигурация
Все TTL и сроки хранения настраиваются через .env:
# Access-токен (JWT)
ACCESS_TOKEN_TTL_MINUTES=15
# Refresh-токен (JWT + Redis)
REFRESH_TOKEN_TTL_DAYS=30
# Токен подтверждения email
EMAIL_VERIFICATION_TTL_HOURS=24
# JWT secret (используется для подписи всех токенов)
JWT_SECRET=your-secret-key-here
# Алгоритм хэширования паролей (argon2)
ARGON2_TIME_COST=2
ARGON2_MEMORY_COST=19 # 2^19 КБ = 512 МБ
ARGON2_PARALLELISM=1
Связанные файлы
- Реализация:
backend/api/auth.py,backend/api/users.py,backend/api/deps.py - Бизнес-логика:
backend/services/auth.py - Безопасность:
backend/core/security.py(token creation/verification, password hashing) - Модели:
backend/models/user.py,backend/models/email_verification.py - Схемы:
backend/schemas/auth.py - Тесты:
backend/tests/test_auth.py,backend/tests/test_rbac.py