Перейти к содержанию

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 систему аутентификации:

  1. User Authentication: Email/Password + Google OAuth (Supabase Auth) → Saga JWT tokens
  2. 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:

  1. JWKS Verification: Проверяет подпись Supabase JWT через JWKS endpoint
  2. Email Extraction: Извлекает email из JWT claims (MANDATORY)
  3. User Creation/Find: Создаёт или находит пользователя по email в БД
  4. Admin Check: Проверяет email в config.yaml ADMIN_EMAILS
  5. 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 metadata
  • is_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:

POST /api/auth/supabase/login
Authorization: Bearer <supabase_jwt_for_admin_email>

Admin Response:

{
  "success": true,
  "data": {
    "token": "saga_admin_jwt_token",
    "user": { "id": "...", "email": "admin@saga-test.com" },
    "isAdmin": true,
    "adminPermissions": ["*"]
  }
}

Get Current User

GET /api/auth/user/profile
Authorization: Bearer <saga_jwt>

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

POST /api/auth/logout
Authorization: Bearer <saga_jwt>

Response:

{
  "success": true,
  "message": "Logged out successfully"
}


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"]

Implementation:

Architecture:

External Integrations:


✍️ 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