330 lines
12 KiB
Markdown
330 lines
12 KiB
Markdown
# OAuth Token Management
|
||
|
||
## Overview
|
||
Система управления OAuth токенами с использованием Redis для безопасного и производительного хранения токенов доступа и обновления от различных провайдеров.
|
||
|
||
## Архитектура
|
||
|
||
### Redis Storage
|
||
OAuth токены хранятся в Redis с автоматическим истечением (TTL):
|
||
- `oauth_access:{user_id}:{provider}` - access tokens
|
||
- `oauth_refresh:{user_id}:{provider}` - refresh tokens
|
||
|
||
### Поддерживаемые провайдеры
|
||
- Google OAuth 2.0
|
||
- Facebook Login
|
||
- GitHub OAuth
|
||
|
||
## API Documentation
|
||
|
||
### OAuthTokenStorage Class
|
||
|
||
#### store_access_token()
|
||
Сохраняет access token в Redis с автоматическим TTL.
|
||
|
||
```python
|
||
await OAuthTokenStorage.store_access_token(
|
||
user_id=123,
|
||
provider="google",
|
||
access_token="ya29.a0AfH6SM...",
|
||
expires_in=3600,
|
||
additional_data={"scope": "profile email"}
|
||
)
|
||
```
|
||
|
||
#### store_refresh_token()
|
||
Сохраняет refresh token с длительным TTL (30 дней по умолчанию).
|
||
|
||
```python
|
||
await OAuthTokenStorage.store_refresh_token(
|
||
user_id=123,
|
||
provider="google",
|
||
refresh_token="1//04...",
|
||
ttl=2592000 # 30 дней
|
||
)
|
||
```
|
||
|
||
#### get_access_token()
|
||
Получает действующий access token из Redis.
|
||
|
||
```python
|
||
token_data = await OAuthTokenStorage.get_access_token(123, "google")
|
||
if token_data:
|
||
access_token = token_data["token"]
|
||
expires_in = token_data["expires_in"]
|
||
```
|
||
|
||
#### refresh_access_token()
|
||
Обновляет access token (и опционально refresh token).
|
||
|
||
```python
|
||
success = await OAuthTokenStorage.refresh_access_token(
|
||
user_id=123,
|
||
provider="google",
|
||
new_access_token="ya29.new_token...",
|
||
expires_in=3600,
|
||
new_refresh_token="1//04new..." # опционально
|
||
)
|
||
```
|
||
|
||
#### delete_tokens()
|
||
Удаляет все токены пользователя для провайдера.
|
||
|
||
```python
|
||
await OAuthTokenStorage.delete_tokens(123, "google")
|
||
```
|
||
|
||
#### get_user_providers()
|
||
Получает список OAuth провайдеров для пользователя.
|
||
|
||
```python
|
||
providers = await OAuthTokenStorage.get_user_providers(123)
|
||
# ["google", "github"]
|
||
```
|
||
|
||
#### extend_token_ttl()
|
||
Продлевает срок действия токена.
|
||
|
||
```python
|
||
# Продлить access token на 30 минут
|
||
success = await OAuthTokenStorage.extend_token_ttl(123, "google", "access", 1800)
|
||
|
||
# Продлить refresh token на 7 дней
|
||
success = await OAuthTokenStorage.extend_token_ttl(123, "google", "refresh", 604800)
|
||
```
|
||
|
||
#### get_token_info()
|
||
Получает подробную информацию о токенах включая TTL.
|
||
|
||
```python
|
||
info = await OAuthTokenStorage.get_token_info(123, "google")
|
||
# {
|
||
# "user_id": 123,
|
||
# "provider": "google",
|
||
# "access_token": {"exists": True, "ttl": 3245},
|
||
# "refresh_token": {"exists": True, "ttl": 2589600}
|
||
# }
|
||
```
|
||
|
||
## Data Structures
|
||
|
||
### Access Token Structure
|
||
```json
|
||
{
|
||
"token": "ya29.a0AfH6SM...",
|
||
"provider": "google",
|
||
"user_id": 123,
|
||
"created_at": 1640995200,
|
||
"expires_in": 3600,
|
||
"scope": "profile email",
|
||
"token_type": "Bearer"
|
||
}
|
||
```
|
||
|
||
### Refresh Token Structure
|
||
```json
|
||
{
|
||
"token": "1//04...",
|
||
"provider": "google",
|
||
"user_id": 123,
|
||
"created_at": 1640995200
|
||
}
|
||
```
|
||
|
||
## Security Considerations
|
||
|
||
### Token Expiration
|
||
- **Access tokens**: TTL основан на `expires_in` от провайдера (обычно 1 час)
|
||
- **Refresh tokens**: TTL 30 дней по умолчанию
|
||
- **Автоматическая очистка**: Redis автоматически удаляет истекшие токены
|
||
- **Внутренняя система истечения**: Использует SET + EXPIRE для точного контроля TTL
|
||
|
||
### Redis Expiration Benefits
|
||
- **Гибкость**: Можно изменять TTL существующих токенов через EXPIRE
|
||
- **Мониторинг**: Команда TTL показывает оставшееся время жизни токена
|
||
- **Расширение**: Возможность продления срока действия токенов без перезаписи
|
||
- **Атомарность**: Separate SET/EXPIRE operations для лучшего контроля
|
||
|
||
### Access Control
|
||
- Токены доступны только владельцу аккаунта
|
||
- Нет доступа к токенам через GraphQL API
|
||
- Токены не хранятся в основной базе данных
|
||
|
||
### Provider Isolation
|
||
- Токены разных провайдеров хранятся отдельно
|
||
- Удаление токенов одного провайдера не влияет на другие
|
||
- Поддержка множественных OAuth подключений
|
||
|
||
## Integration Examples
|
||
|
||
### OAuth Login Flow
|
||
```python
|
||
# После успешной авторизации через OAuth провайдера
|
||
async def handle_oauth_callback(user_id: int, provider: str, tokens: dict):
|
||
# Сохраняем токены в Redis
|
||
await OAuthTokenStorage.store_access_token(
|
||
user_id=user_id,
|
||
provider=provider,
|
||
access_token=tokens["access_token"],
|
||
expires_in=tokens.get("expires_in", 3600)
|
||
)
|
||
|
||
if "refresh_token" in tokens:
|
||
await OAuthTokenStorage.store_refresh_token(
|
||
user_id=user_id,
|
||
provider=provider,
|
||
refresh_token=tokens["refresh_token"]
|
||
)
|
||
```
|
||
|
||
### Token Refresh
|
||
```python
|
||
async def refresh_oauth_token(user_id: int, provider: str):
|
||
# Получаем refresh token
|
||
refresh_data = await OAuthTokenStorage.get_refresh_token(user_id, provider)
|
||
if not refresh_data:
|
||
return False
|
||
|
||
# Обмениваем refresh token на новый access token
|
||
new_tokens = await exchange_refresh_token(
|
||
provider, refresh_data["token"]
|
||
)
|
||
|
||
# Сохраняем новые токены
|
||
return await OAuthTokenStorage.refresh_access_token(
|
||
user_id=user_id,
|
||
provider=provider,
|
||
new_access_token=new_tokens["access_token"],
|
||
expires_in=new_tokens.get("expires_in"),
|
||
new_refresh_token=new_tokens.get("refresh_token")
|
||
)
|
||
```
|
||
|
||
### API Integration
|
||
```python
|
||
async def make_oauth_request(user_id: int, provider: str, endpoint: str):
|
||
# Получаем действующий access token
|
||
token_data = await OAuthTokenStorage.get_access_token(user_id, provider)
|
||
|
||
if not token_data:
|
||
# Токен отсутствует, требуется повторная авторизация
|
||
raise OAuthTokenMissing()
|
||
|
||
# Делаем запрос к API провайдера
|
||
headers = {"Authorization": f"Bearer {token_data['token']}"}
|
||
response = await httpx.get(endpoint, headers=headers)
|
||
|
||
if response.status_code == 401:
|
||
# Токен истек, пытаемся обновить
|
||
if await refresh_oauth_token(user_id, provider):
|
||
# Повторяем запрос с новым токеном
|
||
token_data = await OAuthTokenStorage.get_access_token(user_id, provider)
|
||
headers = {"Authorization": f"Bearer {token_data['token']}"}
|
||
response = await httpx.get(endpoint, headers=headers)
|
||
|
||
return response.json()
|
||
```
|
||
|
||
### TTL Monitoring and Management
|
||
```python
|
||
async def monitor_token_expiration(user_id: int, provider: str):
|
||
"""Мониторинг и управление сроком действия токенов"""
|
||
|
||
# Получаем информацию о токенах
|
||
info = await OAuthTokenStorage.get_token_info(user_id, provider)
|
||
|
||
# Проверяем access token
|
||
if info["access_token"]["exists"]:
|
||
ttl = info["access_token"]["ttl"]
|
||
if ttl < 300: # Меньше 5 минут
|
||
logger.warning(f"Access token expires soon: {ttl}s")
|
||
# Автоматически обновляем токен
|
||
await refresh_oauth_token(user_id, provider)
|
||
|
||
# Проверяем refresh token
|
||
if info["refresh_token"]["exists"]:
|
||
ttl = info["refresh_token"]["ttl"]
|
||
if ttl < 86400: # Меньше 1 дня
|
||
logger.warning(f"Refresh token expires soon: {ttl}s")
|
||
# Уведомляем пользователя о необходимости повторной авторизации
|
||
|
||
async def extend_session_if_active(user_id: int, provider: str):
|
||
"""Продлевает сессию для активных пользователей"""
|
||
|
||
# Проверяем активность пользователя
|
||
if await is_user_active(user_id):
|
||
# Продлеваем access token на 1 час
|
||
success = await OAuthTokenStorage.extend_token_ttl(
|
||
user_id, provider, "access", 3600
|
||
)
|
||
if success:
|
||
logger.info(f"Extended access token for active user {user_id}")
|
||
```
|
||
|
||
## Migration from Database
|
||
|
||
Если у вас уже есть OAuth токены в базе данных, используйте этот скрипт для миграции:
|
||
|
||
```python
|
||
async def migrate_oauth_tokens():
|
||
"""Миграция OAuth токенов из БД в Redis"""
|
||
with local_session() as session:
|
||
# Предполагая, что токены хранились в таблице authors
|
||
authors = session.query(Author).filter(
|
||
or_(
|
||
Author.provider_access_token.is_not(None),
|
||
Author.provider_refresh_token.is_not(None)
|
||
)
|
||
).all()
|
||
|
||
for author in authors:
|
||
# Получаем провайдер из oauth вместо старого поля oauth
|
||
if author.oauth:
|
||
for provider in author.oauth.keys():
|
||
if author.provider_access_token:
|
||
await OAuthTokenStorage.store_access_token(
|
||
user_id=author.id,
|
||
provider=provider,
|
||
access_token=author.provider_access_token
|
||
)
|
||
|
||
if author.provider_refresh_token:
|
||
await OAuthTokenStorage.store_refresh_token(
|
||
user_id=author.id,
|
||
provider=provider,
|
||
refresh_token=author.provider_refresh_token
|
||
)
|
||
|
||
print(f"Migrated OAuth tokens for {len(authors)} authors")
|
||
```
|
||
|
||
## Performance Benefits
|
||
|
||
### Redis Advantages
|
||
- **Скорость**: Доступ к токенам за микросекунды
|
||
- **Масштабируемость**: Не нагружает основную БД
|
||
- **Автоматическая очистка**: TTL убирает истекшие токены
|
||
- **Память**: Эффективное использование памяти Redis
|
||
|
||
### Reduced Database Load
|
||
- OAuth токены больше не записываются в основную БД
|
||
- Уменьшено количество записей в таблице authors
|
||
- Faster user queries без JOIN к токенам
|
||
|
||
## Monitoring and Maintenance
|
||
|
||
### Redis Memory Usage
|
||
```bash
|
||
# Проверка использования памяти OAuth токенами
|
||
redis-cli --scan --pattern "oauth_*" | wc -l
|
||
redis-cli memory usage oauth_access:123:google
|
||
```
|
||
|
||
### Cleanup Statistics
|
||
```python
|
||
# Периодическая очистка и логирование (опционально)
|
||
async def oauth_cleanup_job():
|
||
cleaned = await OAuthTokenStorage.cleanup_expired_tokens()
|
||
logger.info(f"OAuth cleanup completed, {cleaned} tokens processed")
|
||
```
|