Email-First Authentication: Integration-Only Platform¶
Аудитория: разработчики, backend-инженеры, специалисты по безопасности Последнее обновление: 2025-11-17 Архитектура: Integration-Only (EMAIL-FIRST MANDATORY) Краткое содержание: Полное руководство по системе аутентификации Saga — email-first Supabase Auth для пользователей, JWT token management, admin authentication и security best practices без Web3 complexity.
Архитектурные принципы (Integration-Only)¶
Saga использует упрощенную email-first систему аутентификации:
- User Authentication: Email/Password + Google OAuth (Supabase Auth) → Saga JWT tokens
- Admin Authentication: Email-based конфигурация (config.yaml) → Admin JWT tokens
Ключевые принципы:
- ✅ Email MANDATORY: Email как единственный identifier пользователя
- ✅ No Wallet Complexity: Никаких MetaMask, private keys, signature flows
- ✅ Supabase Integration: Professional OAuth + email verification
- ✅ External Custody: Crypto операции через Crypto2B + Fordefi (не user-managed)
- ✅ Short-Lived Tokens: 24-часовое истечение JWT для безопасности
- ✅ Config-Based Admins: Админы в config.yaml (не в БД)
Email-First User Authentication Flow¶
Пошаговый процесс (Integration-Only)¶
1. Пользователь регистрируется через email (Frontend)
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
// Опция 1: Email + Password регистрация
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
});
// Опция 2: Google OAuth (рекомендуется)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://app.saga.surf/auth/callback'
}
});
2. Supabase возвращает JWT access_token
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "rt_abc123...",
"user": {
"id": "user-uuid-123",
"email": "user@example.com",
"email_verified": true,
"user_metadata": {
"full_name": "John Doe"
}
}
}
3. Frontend отправляет Supabase JWT к Saga Backend
POST /api/auth/supabase/login
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
{
"redirectUrl": "/dashboard"
}
4. Backend верифицирует Supabase JWT и создает Saga JWT
Процесс Backend:
- JWKS Verification: Проверяет подпись Supabase JWT через JWKS endpoint
- Email Extraction: Извлекает email из JWT claims (MANDATORY)
- User Creation/Find: Создаёт или находит пользователя по email в БД
- Admin Check: Проверяет email в config.yaml ADMIN_EMAILS
- Saga JWT Creation: Генерирует внутренний JWT с email claims
// Псевдокод Go backend
func (s *AuthService) VerifySupabaseLogin(ctx context.Context, supabaseJWT string) (*AuthResponse, error) {
// 1. Верификация Supabase JWT
claims, err := s.supabaseHandler.VerifyJWT(supabaseJWT)
if err != nil {
return nil, errors.Wrap(err, "invalid Supabase JWT")
}
// 2. Извлечение email (MANDATORY)
email := claims.Email
if email == "" {
return nil, errors.New("email is MANDATORY - not found in JWT claims")
}
// 3. User creation/retrieval
user, err := s.userRepo.FindOrCreateByEmail(ctx, email, claims.UserMetadata.FullName)
if err != nil {
return nil, errors.Wrap(err, "user creation failed")
}
// 4. Admin check via config
isAdmin := s.config.IsAdminEmail(email)
// 5. Generate Saga JWT
sagaJWT, err := s.jwtManager.CreateToken(user.ID, email, isAdmin)
if err != nil {
return nil, errors.Wrap(err, "JWT creation failed")
}
return &AuthResponse{
Token: sagaJWT,
User: user,
IsAdmin: isAdmin,
}, nil
}
Успешный ответ Saga Backend:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 86400,
"user": {
"id": "user_123abc",
"email": "user@example.com",
"full_name": "John Doe",
"created_at": "2025-11-13T10:00:00Z"
},
"isAdmin": false
}
}
5. Frontend сохраняет Saga JWT для API вызовов
// Сохранение Saga JWT
localStorage.setItem('saga_auth_token', response.data.token);
// Использование в API запросах
const sagaToken = localStorage.getItem('saga_auth_token');
fetch('/api/user/investments', {
headers: {
'Authorization': `Bearer ${sagaToken}`
}
});
🔑 Структура Saga JWT токена (Integration-Only)¶
User JWT Claims¶
{
"user_id": "user_123abc",
"email": "user@example.com",
"full_name": "John Doe",
"is_admin": false,
"iat": 1699876800,
"exp": 1699963200,
"iss": "saga-platform"
}
Описание полей (Integration-Only):
user_id: User ID в Saga database (primary key)email: PRIMARY IDENTIFIER - email адрес пользователя (MANDATORY)full_name: Полное имя пользователя из Supabase metadatais_admin: Boolean флаг админских прав (из config.yaml)iat(Issued At): Unix timestamp создания токенаexp(Expiration): Unix timestamp истечения (24 часа после iat)iss(Issuer): "saga-platform" (константа)
Admin JWT Claims¶
{
"user_id": "admin_456def",
"email": "admin@saga-test.com",
"full_name": "Admin User",
"is_admin": true,
"admin_permissions": ["*"],
"iat": 1699876800,
"exp": 1699963200,
"iss": "saga-platform"
}
Дополнительные поля админа:
is_admin: true (критично для middleware проверки)admin_permissions: Массив разрешений ("*" = все права)- Email должен быть в config/auth.yaml ADMIN_EMAILS
Email-Based Admin Authentication¶
Admin Configuration (config/auth.yaml)¶
ПРИНЦИП: Админы НЕ в базе данных, только в конфигурации
# config/auth.yaml
auth:
admin_emails:
- "admin@saga-test.com" # Primary admin
- "operator@saga-test.com" # Operations admin
- "finance@saga-test.com" # Finance admin
supabase:
url: "https://your-project.supabase.co"
service_role_key: "SERVICE_ROLE_KEY"
jwt_secret: "JWT_SECRET_FROM_SUPABASE"
Admin Login Flow¶
1. Admin пытается войти через обычный Supabase flow
// Тот же процесс что для обычных пользователей
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google'
});
// Или email/password
const { data, error } = await supabase.auth.signInWithPassword({
email: 'admin@saga-test.com',
password: 'admin-password'
});
2. Backend проверяет email в config при создании JWT
func (s *AuthService) VerifySupabaseLogin(ctx context.Context, supabaseJWT string) (*AuthResponse, error) {
// ... стандартная верификация ...
email := claims.Email
// Проверка админского статуса через конфигурацию
isAdmin := s.config.Auth.IsAdminEmail(email)
if isAdmin {
log.Info("Admin login detected", "email", email)
// Генерация JWT с admin claims
sagaJWT, err := s.jwtManager.CreateAdminToken(user.ID, email)
} else {
// Генерация обычного user JWT
sagaJWT, err := s.jwtManager.CreateUserToken(user.ID, email)
}
return &AuthResponse{
Token: sagaJWT,
User: user,
IsAdmin: isAdmin,
}, nil
}
3. Admin JWT Response
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 86400,
"user": {
"id": "admin_456def",
"email": "admin@saga-test.com",
"full_name": "Admin User"
},
"isAdmin": true,
"adminPermissions": ["*"]
}
}
JWT Middleware Protection (Integration-Only)¶
Standard User Endpoints Protection¶
func (m *AuthMiddleware) RequireUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Извлечение токена из Authorization header
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// 2. Верификация Saga JWT
claims, err := m.jwtManager.VerifyToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// 3. Проверка email (MANDATORY)
if claims.Email == "" {
http.Error(w, "Email is MANDATORY", http.StatusUnauthorized)
return
}
// 4. Поиск пользователя по email
user, err := m.userRepo.FindByEmail(r.Context(), claims.Email)
if err != nil {
http.Error(w, "User not found", http.StatusUnauthorized)
return
}
// 5. Добавление пользователя в контекст
ctx := context.WithValue(r.Context(), "user", user)
ctx = context.WithValue(ctx, "email", claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Admin Endpoints Protection¶
func (m *AuthMiddleware) RequireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Стандартная проверка токена...
claims, err := m.jwtManager.VerifyToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Проверка админских прав
if !claims.IsAdmin {
http.Error(w, "Admin access required", http.StatusForbidden)
return
}
// Дополнительная проверка email в config (double security)
if !m.config.Auth.IsAdminEmail(claims.Email) {
log.Warn("Admin token but email not in config", "email", claims.Email)
http.Error(w, "Admin access denied", http.StatusForbidden)
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
API Endpoints Reference¶
User Authentication¶
POST /api/auth/supabase/login
Authorization: Bearer <supabase_jwt>
Content-Type: application/json
{
"redirectUrl": "/dashboard"
}
Response:
{
"success": true,
"data": {
"token": "saga_jwt_token",
"user": { "id": "...", "email": "..." },
"isAdmin": false
}
}
Admin Authentication¶
Тот же endpoint, но результат зависит от email в config:
Admin Response:
{
"success": true,
"data": {
"token": "saga_admin_jwt_token",
"user": { "id": "...", "email": "admin@saga-test.com" },
"isAdmin": true,
"adminPermissions": ["*"]
}
}
Get Current User¶
Response:
{
"success": true,
"data": {
"id": "user_123",
"email": "user@example.com",
"full_name": "John Doe",
"created_at": "2025-11-13T10:00:00Z",
"last_login_at": "2025-11-13T15:30:00Z"
}
}
Logout¶
Response:
Testing Authentication (Integration-Only)¶
cURL Examples¶
1. User Login (требуется реальный Supabase JWT):
# Получите Supabase JWT через frontend flow, затем:
SUPABASE_JWT="eyJhbGciOiJSUzI1NiIs..."
curl -X POST https://app.saga.surf/api/auth/supabase/login \
-H "Authorization: Bearer $SUPABASE_JWT" \
-H "Content-Type: application/json" \
-d '{"redirectUrl": "/dashboard"}'
2. Использование Saga JWT:
SAGA_JWT="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://app.saga.surf/api/user/investments \
-H "Authorization: Bearer $SAGA_JWT"
3. Admin endpoint access:
# Используйте admin email в Supabase login, получите admin Saga JWT
ADMIN_JWT="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://app.saga.surf/api/admin/users \
-H "Authorization: Bearer $ADMIN_JWT"
Frontend Integration Example¶
import { createClient } from '@supabase/supabase-js';
import { SagaApiClient } from '@saga/api-client';
class AuthService {
private supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
private sagaApi = new SagaApiClient();
async loginWithGoogle(): Promise<{ sagaToken: string, user: any, isAdmin: boolean }> {
// 1. Supabase OAuth login
const { data, error } = await this.supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) throw error;
// 2. Get Supabase session after redirect
const { data: session } = await this.supabase.auth.getSession();
if (!session?.session?.access_token) {
throw new Error('No Supabase session found');
}
// 3. Exchange Supabase JWT for Saga JWT
const response = await this.sagaApi.post('/auth/supabase/login',
{ redirectUrl: '/dashboard' },
{ headers: { Authorization: `Bearer ${session.session.access_token}` } }
);
// 4. Store Saga JWT
localStorage.setItem('saga_auth_token', response.data.token);
this.sagaApi.setAuthToken(response.data.token);
return {
sagaToken: response.data.token,
user: response.data.user,
isAdmin: response.data.isAdmin
};
}
async loginWithEmail(email: string, password: string) {
// Similar flow but with email/password
const { data, error } = await this.supabase.auth.signInWithPassword({
email,
password
});
if (error) throw error;
// Continue with JWT exchange...
}
async logout() {
// 1. Logout from Supabase
await this.supabase.auth.signOut();
// 2. Clear Saga JWT
localStorage.removeItem('saga_auth_token');
this.sagaApi.clearAuthToken();
}
}
Security Best Practices (Integration-Only)¶
For Developers¶
✅ ДЕЛАЙТЕ:
- Всегда верифицируйте Supabase JWT через JWKS (не доверяйте токенам)
- Используйте email как PRIMARY IDENTIFIER во всех системах
- Храните админские emails только в config.yaml (не в БД)
- Реализуйте rate limiting на auth endpoints
- Используйте HTTPS в production (Let's Encrypt)
- Логируйте все authentication события для audit trail
- Ротируйте JWT секреты периодически (ежеквартально)
- Используйте httpOnly cookies для хранения JWT (защита от XSS)
❌ НЕ ДЕЛАЙТЕ:
- Не храните админский статус в database (только config.yaml)
- Не доверяйте email из frontend - берите только из верифицированного JWT
- Не игнорируйте Supabase email verification status
- Не захардкоживайте admin emails в коде (только конфигурация)
- Не логируйте JWT токены или пароли
- Не используйте localStorage для JWT в production (используйте httpOnly cookies)
Email Security¶
Email Mandatory Enforcement:
// Функция должна быть везде где работаем с пользователями
func validateEmailMandatory(email string) error {
if strings.TrimSpace(email) == "" {
return errors.New("email is MANDATORY - cannot be empty")
}
if !isValidEmail(email) {
return errors.New("email format is invalid")
}
return nil
}
// Все User-related операции должны проверять email
func (s *UserService) CreateUser(ctx context.Context, email, fullName string) error {
if err := validateEmailMandatory(email); err != nil {
return errors.Wrap(err, "user creation failed")
}
// Continue with user creation...
}
Supabase Security¶
JWKS Verification:
type SupabaseHandler struct {
jwksURL string
projectURL string
jwtSecret string
}
func (h *SupabaseHandler) VerifyJWT(tokenString string) (*SupabaseClaims, error) {
// 1. Get JWKS from Supabase
jwks, err := h.getJWKS()
if err != nil {
return nil, errors.Wrap(err, "failed to fetch JWKS")
}
// 2. Parse and verify JWT signature
token, err := jwt.ParseWithClaims(tokenString, &SupabaseClaims{}, func(token *jwt.Token) (interface{}, error) {
return jwks.GetKey(token.Header["kid"].(string))
})
if err != nil {
return nil, errors.Wrap(err, "JWT verification failed")
}
// 3. Validate claims
claims := token.Claims.(*SupabaseClaims)
if claims.Email == "" {
return nil, errors.New("email claim is MANDATORY")
}
if !claims.EmailVerified {
return nil, errors.New("email must be verified")
}
return claims, nil
}
Troubleshooting Guide (Integration-Only)¶
Error: "Email is MANDATORY - not found in JWT claims"¶
Причины: - Supabase JWT не содержит email claim - User отозвал разрешение на email в OAuth - Некорректная настройка Supabase проекта
Решение:
# 1. Проверьте Supabase project settings
# 2. Убедитесь что email verification включен
# 3. Проверьте OAuth provider настройки (Google требует email scope)
# Декодируйте JWT для проверки claims:
echo "JWT_TOKEN" | cut -d. -f2 | base64 -d | jq .
Error: "Admin access denied"¶
Причины: - Email не в config/auth.yaml ADMIN_EMAILS - Неправильный формат email в конфиге - JWT подделан (email claim изменен)
Решение:
# Проверьте admin emails в конфиге
rg -n "admin_emails" ./config/
# Проверьте JWT claims
jwt_decode() {
echo "$1" | cut -d. -f2 | base64 -d | jq .
}
jwt_decode "$SAGA_JWT"
Error: "Invalid Supabase JWT"¶
Причины: - JWT подпись не прошла JWKS верификацию - JWT истёк (Supabase tokens = 1 час) - Неправильные Supabase credentials
Решение:
# Проверьте Supabase connectivity
curl -X GET https://your-project.supabase.co/.well-known/jwks.json
# Проверьте Supabase project settings
echo $NEXT_PUBLIC_SUPABASE_URL
echo $SUPABASE_SERVICE_ROLE_KEY
Error: "User not found" after JWT verification¶
Причины: - Пользователь не создан в Saga database - Email mismatch между Supabase и Saga - Database connection issues
Решение:
-- Проверьте users в database
SELECT id, email, created_at FROM users WHERE email = 'user@example.com';
-- Проверьте email constraints
\d users;
Monitoring and Metrics¶
Authentication Success Rates (SLA Targets)¶
- Supabase JWT Verification: 99.9% успешность
- User Creation/Lookup: 99.8% успешность
- Saga JWT Generation: 99.9% успешность
- Admin Email Validation: 100% успешность
Performance Targets¶
- Supabase JWKS Fetch: < 200ms (кешируется на 1 час)
- JWT Verification: < 100ms
- Database User Lookup: < 50ms
- Saga JWT Generation: < 50ms
Key Metrics to Monitor¶
# Metrics для Prometheus
metrics:
- name: "saga_auth_requests_total"
labels: ["method", "endpoint", "status"]
- name: "saga_auth_jwt_verification_duration_seconds"
labels: ["provider"] # supabase, saga
- name: "saga_auth_admin_access_attempts_total"
labels: ["email", "success"]
- name: "saga_auth_email_mandatory_violations_total"
labels: ["endpoint"]
Related Documentation (Integration-Only)¶
Implementation:
- User Auth API Endpoints - Email-first authentication endpoints
- Admin Auth API Endpoints - Config-based admin authentication
- Error Handling - Authentication error codes и handling
Architecture:
- Authentication Flow - Detailed Integration-Only flow
- Database Schema - Email mandatory principles
- Integration-Only Architecture - Core principles
External Integrations:
- Supabase Auth Documentation - Official Supabase guides
- JWT.io - JWT token debugging
✍️ Document Information¶
Author: Saga Development Team Contributors: Backend Engineer, Security Specialist, Integration Architect Architecture: Integration-Only (Email-First Mandatory, No Web3 Complexity)
"Simplicity is the ultimate sophistication — authentication should be invisible to users, bulletproof for developers."
— Saga Engineering Team (Integration-Only Principles)
📋 Метаданные¶
Версия: 2.6.268 Обновлено: 2025-11-13 Статус: Ready for Implementation Архитектура: Email-First, Supabase Auth, Config-Based Admins