Сучасний мікросервіс для централізованої автентифікації та авторизації користувачів у мікросервісній архітектурі. Побудований на FastAPI, PostgreSQL, Redis та OpenFGA.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │────▶│ API Gateway │────▶│Auth Service │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
┌─────▼─────┐ ┌───────▼───────┐ ┌───────▼───────┐
│PostgreSQL │ │ Redis │ │ OpenFGA │
└───────────┘ └───────────────┘ └───────────────┘
-
Реєстрація користувача
- Користувач надсилає дані реєстрації
- Система перевіряє унікальність email/username
- Пароль хешується через Argon2id
- Створюється запис користувача в PostgreSQL
- Надсилається email для верифікації (якщо увімкнено)
-
Вхід в систему
- Користувач вводить email і пароль
- Система перевіряє credentials
- Якщо MFA увімкнено - запитує OTP код
- Генерується JWT access token (15 хв) і refresh token (30 днів)
- Токени зберігаються в Redis для швидкої валідації
-
Авторизація запитів
- Кожен запит містить JWT token в заголовку Authorization
- Token перевіряється на підпис і термін дії
- З токена витягується user_id і scopes
- Для складних прав - запит до OpenFGA
-
Оновлення токенів
- Коли access token закінчується, клієнт використовує refresh token
- Старий refresh token додається в blacklist
- Видається нова пара токенів
Що робить: Реєструє нового користувача в системі
Запит:
{
"email": "user@example.com",
"username": "john_doe", // опціонально
"full_name": "John Doe", // опціонально
"password": "SecurePass123!"
}Відповідь:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "john_doe",
"full_name": "John Doe",
"status": "inactive", // або "active" якщо email верифікація вимкнена
"is_email_verified": false,
"is_superuser": false,
"mfa_enabled": false,
"created_at": "2024-01-15T10:30:00Z"
}Що відбувається всередині:
- Валідація даних (пароль мін 8 символів, має великі/малі літери і цифри)
- Перевірка чи email/username вже існують
- Хешування пароля через Argon2id
- Збереження в БД
- Відправка email для верифікації (якщо увімкнено)
Що робить: Вхід користувача, отримання токенів
Запит (OAuth2 формат):
username: user@example.com # Так, це email, але OAuth2 вимагає поле "username"
password: SecurePass123!
grant_type: password
scope: openid profile email # опціонально
Відповідь (без MFA):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900, // 15 хвилин в секундах
"scope": "openid profile email"
}Відповідь (з MFA):
{
"access_token": "temp_token_for_mfa",
"token_type": "mfa_required",
"expires_in": 300 // 5 хвилин для введення MFA
}Що відбувається всередині:
- Пошук користувача по email
- Перевірка чи акаунт не заблокований
- Перевірка пароля
- Якщо MFA увімкнено - створення тимчасової сесії в Redis
- Якщо MFA вимкнено - генерація JWT токенів
- Створення сесії в БД з IP адресою і User-Agent
Що робить: Перевіряє MFA код після логіну
Запит:
{
"token": "123456", // 6-значний TOTP код
"temp_token": "temp_token_from_login"
}Відповідь: Така ж як у /token без MFA
Що робить: Оновлює access token використовуючи refresh token
Запит:
{
"refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}Відповідь:
{
"access_token": "new_access_token",
"refresh_token": "new_refresh_token",
"token_type": "Bearer",
"expires_in": 900
}Що відбувається всередині:
- Перевірка refresh token на валідність
- Додавання старого refresh token в Redis blacklist
- Генерація нової пари токенів
Що робить: Виходить з системи, анулює всі сесії
Запит: Потребує Authorization: Bearer {access_token} заголовок
Відповідь:
{
"message": "Logged out successfully. 3 sessions revoked."
}Що робить: Повертає базову інформацію про поточного користувача (OIDC endpoint)
Відповідь:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"preferred_username": "john_doe"
}Що робить: Починає процес увімкнення 2FA
Запит:
{
"password": "SecurePass123!" // Для підтвердження
}Відповідь:
{
"secret": "JBSWY3DPEHPK3PXP", // Secret для TOTP
"qr_code": "data:image/png;base64,iVBORw0KGgoAAAA...", // QR код для Google Authenticator
"backup_codes": [
"1234-5678",
"2345-6789",
// ... ще 8 кодів
]
}Важливо для фронтенду:
- Покажіть QR код користувачу
- Дайте можливість скопіювати secret вручну
- Обов'язково покажіть backup коди і попросіть зберегти
Що робить: Підтверджує увімкнення MFA
Запит:
{
"token": "123456" // Код з Google Authenticator
}Що робить: Вимикає MFA
Запит:
{
"password": "SecurePass123!",
"token": "123456" // Поточний MFA код
}Що робить: Повертає повний профіль поточного користувача
Відповідь:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "john_doe",
"full_name": "John Doe",
"status": "active",
"is_email_verified": true,
"is_superuser": false,
"mfa_enabled": true,
"oauth_providers": {
"google": true,
"github": false
},
"metadata_json": {
"preferences": {...},
"settings": {...}
},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T14:20:00Z",
"last_login_at": "2024-01-20T09:15:00Z"
}Що робить: Оновлює дані профілю
Запит:
{
"full_name": "John Smith",
"username": "john_smith"
// email не можна змінити без верифікації
}Що робить: Показує всі активні сесії користувача
Відповідь:
[
{
"id": "session-id-1",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"device_info": {
"browser": "Chrome",
"os": "Windows 10"
},
"created_at": "2024-01-20T09:15:00Z",
"last_activity": "2024-01-20T10:30:00Z",
"expires_at": "2024-01-20T09:30:00Z"
}
]Що робить: Завершує конкретну сесію (наприклад, вихід на іншому пристрої)
Що робить: Запит на скидання пароля
Запит:
{
"email": "user@example.com"
}Відповідь: Завжди успішна (щоб не розкривати чи існує email)
{
"message": "If the email exists, a password reset link has been sent"
}Що робить: Встановлює новий пароль
Запит:
{
"token": "reset_token_from_email",
"new_password": "NewSecurePass123!"
}// Логін
const login = async (email, password) => {
const formData = new FormData();
formData.append('username', email); // Так, це email!
formData.append('password', password);
formData.append('grant_type', 'password');
const response = await fetch('http://localhost:8000/api/v1/auth/token', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.token_type === 'mfa_required') {
// Перенаправити на сторінку MFA
localStorage.setItem('mfa_temp_token', data.access_token);
return { requiresMFA: true };
}
// Зберегти токени
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return data;
};
// Використання токена в запитах
const fetchUserProfile = async () => {
const token = localStorage.getItem('access_token');
const response = await fetch('http://localhost:8000/api/v1/users/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
// Токен протух, спробувати оновити
await refreshToken();
// Повторити запит
}
return response.json();
};
// Оновлення токена
const refreshToken = async () => {
const refresh = localStorage.getItem('refresh_token');
const response = await fetch('http://localhost:8000/api/v1/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refresh })
});
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
};Коли інші мікросервіси (Google Клас, Розклад, Месенджер) потребують інформацію про користувача:
# В іншому мікросервісі
async def get_current_user(token: str):
# Варіант 1: Перевірити токен локально (потребує публічний ключ)
response = await httpx.get("http://auth-service:8000/.well-known/jwks.json")
public_key = response.json()["keys"][0]
# Варіант 2: Запитати auth-service (простіше але повільніше)
response = await httpx.get(
"http://auth-service:8000/api/v1/auth/userinfo",
headers={"Authorization": f"Bearer {token}"}
)
return response.json()- 60 запитів на хвилину per IP
- 1000 запитів на годину per IP
- При перевищенні - HTTP 429
- Мінімум 8 символів
- Обов'язково: велика літера, мала літера, цифра
- Блокування після 5 невдалих спроб на 30 хвилин
- Access token: 15 хвилин життя
- Refresh token: 30 днів життя
- Токени підписані RS256 (асиметричне шифрування)
- Refresh token rotation (старі токени в blacklist)
# Запустити всі сервіси
docker-compose up -d
# Подивитися логи
docker-compose logs -f auth-service
# Зупинити
docker-compose downapiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
spec:
replicas: 3
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth-service
image: your-registry/auth-service:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: auth-secrets
key: database-url
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10http_requests_total- кількість запитівhttp_request_duration_seconds- час обробкиauth_token_generated_total- згенеровано токенівauth_failed_login_total- невдалі спроби входу
Доступний на http://localhost:16686
- Відстеження повного шляху запиту
- Час виконання кожної операції
- Візуалізація залежностей
Доступні на http://localhost:3001 (admin/admin)
- Кількість активних користувачів
- Швидкість відповіді API
- Помилки автентифікації
- Використання ресурсів
Q: Чому поле username при логіні, а не email? A: Це вимога OAuth2 специфікації. В полі username передаємо email.
Q: Як довго живуть токени? A: Access token - 15 хвилин, Refresh token - 30 днів. Налаштовується через змінні оточення.
Q: Чи можна використовувати без MFA? A: Так, MFA опціональне і вмикається користувачем за бажанням.
Q: Як інтегрувати з Google/GitHub OAuth? A: Додайте CLIENT_ID і CLIENT_SECRET в .env, сервіс автоматично активує OAuth ендпоінти.
Q: Що таке OpenFGA і навіщо воно? A: Це система для складних прав доступу. Наприклад, "користувач X може редагувати документ Y якщо він в групі Z". Для простих ролей (admin/user) достатньо JWT claims.