Первоначальная версия VidConf
This commit is contained in:
513
docs/api/auth.md
Normal file
513
docs/api/auth.md
Normal file
@@ -0,0 +1,513 @@
|
||||
# Аутентификация и авторизация
|
||||
|
||||
Эндпоинты для регистрации, верификации email, входа и управления JWT-токенами.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# 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):**
|
||||
```json
|
||||
{
|
||||
"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`); дефолт `false`
|
||||
- `teams` — список команд, отсортированный по названию; **пустой массив**, если `team_choice_enabled=false` (справочник команд не раскрывается, пока выбор выключен)
|
||||
- `email_domain` — эталонный домен email при включённой настройке инстанса `registration_email_domain_enabled`, иначе `null`
|
||||
|
||||
---
|
||||
|
||||
### POST /api/v1/auth/register
|
||||
|
||||
Зарегистрировать нового пользователя и отправить письмо для подтверждения email.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"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 пользователя по одноразовому токену из письма.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"sub": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"role": "user",
|
||||
"type": "access",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567200
|
||||
}
|
||||
```
|
||||
|
||||
**Содержимое refresh-токена (payload):**
|
||||
```json
|
||||
{
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
**Заголовок ответа:**
|
||||
```
|
||||
Set-Cookie: refresh_token=NEW_JWT; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
- `401 Unauthorized` (`missing_refresh_token`) — cookie `refresh_token` не передана
|
||||
- `401 Unauthorized` (`invalid_refresh_token`) — токен невалиден, просрочен, отозван или уже был использован (reuse)
|
||||
|
||||
**Механика ротации:**
|
||||
1. Backend декодирует refresh-токен и проверяет его `jti` в Redis (`refresh:{jti}` → `user_id`)
|
||||
2. Если `jti` есть в Redis → токен валиден, пользователь существует
|
||||
3. Если `jti` отсутствует → reuse или отозван → `401`
|
||||
4. Старый `jti` немедленно удаляется из Redis
|
||||
5. Выдаётся новый refresh-токен с новым `jti`, добавляется в Redis
|
||||
6. Повторное использование уже потраченного 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):**
|
||||
```json
|
||||
{
|
||||
"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` — требуется аутентификация**
|
||||
```python
|
||||
@router.get("/profile")
|
||||
async def profile(user: Annotated[User, Depends(get_current_user)]) -> UserOut:
|
||||
"""Доступно только аутентифицированным пользователям."""
|
||||
return user
|
||||
```
|
||||
- Возвращает объект User
|
||||
- 401 если токен отсутствует, невалиден или истёк
|
||||
|
||||
**`get_current_user_optional` — опциональная аутентификация (guest = None)**
|
||||
```python
|
||||
@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**
|
||||
```python
|
||||
@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, равный сроку жизни токена
|
||||
|
||||
Это позволяет:
|
||||
1. Отозвать токены без пересоздания ключей подписи
|
||||
2. Обнаружить replay-атаки (повторное использование старого токена)
|
||||
3. Контролировать количество активных сессий per user (если реализовать)
|
||||
|
||||
### Сравнение с альтернативами
|
||||
|
||||
| Подход | Плюсы | Минусы | Используется |
|
||||
|--------|-------|--------|-------------|
|
||||
| httpOnly cookie | Защита от XSS | Требует SameSite для защиты CSRF | ✅ Refresh-токен |
|
||||
| Bearer token в теле | Явный контроль | Уязвимо для XSS | ✅ Access-токен (одноразовый) |
|
||||
| Opaque tokens (session ID) | Компактно | Требует БД на каждый запрос | ❌ Не используется |
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### JavaScript / TypeScript (frontend)
|
||||
```typescript
|
||||
// Регистрация
|
||||
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 примеры
|
||||
|
||||
```bash
|
||||
# Полный цикл регистрации и входа
|
||||
|
||||
# 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`:
|
||||
|
||||
```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`
|
||||
Reference in New Issue
Block a user