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

Authentication Endpoints (Integration-Only)

Аудитория: разработчики, frontend-инженеры, DevOps Последнее обновление: 2025-11-17 Архитектура: Integration-Only (EMAIL-FIRST MANDATORY) Краткое содержание: Email-first authentication endpoints с Supabase Auth integration — простая, безопасная аутентификация без Web3 complexity. Все операции через email identifier.


Архитектурные принципы

Saga Authentication API построен на принципах Integration-Only Architecture:

  • Email MANDATORY: Email как единственный identifier пользователя
  • Supabase Integration: Professional OAuth providers + email verification
  • No Web3 Complexity: Никаких MetaMask, signatures, wallet operations
  • Config-Based Admins: Админы определяются в config.yaml, не в БД
  • Simple JWT Flow: Supabase JWT → Saga JWT exchange

Удаленные концепции (больше не поддерживаются): - ❌ Web3 wallet authentication - ❌ MetaMask signature verification - ❌ ECDSA signature flows - ❌ Wallet address as identifier - ❌ Challenge/response authentication


Обзор Endpoints

User Authentication Endpoints (Integration-Only)

Метод Endpoint Описание Auth требуется
POST /api/auth/supabase/login Основной вход через Supabase JWT ❌ Нет (Supabase JWT)
GET /api/auth/user/profile Получить профиль пользователя ✅ Да (Saga JWT)
POST /api/auth/logout Выход из системы ✅ Да (Saga JWT)
GET /api/auth/status Проверка статуса аутентификации (НИКОГДА 401) ❌ Нет

Admin Authentication Endpoints (Integration-Only)

Метод Endpoint Описание Auth требуется
POST /api/auth/supabase/login Тот же endpoint (email в config определяет admin) ❌ Нет (Supabase JWT)
GET /api/auth/admin/profile Получить admin профиль ✅ Да (Admin Saga JWT)
GET /api/auth/admin/health Health check админской аутентификации ❌ Нет

User Authentication Endpoints

POST /api/auth/supabase/login

Основной endpoint для входа в систему. Принимает Supabase JWT, верифицирует его, создает или находит пользователя по email, и возвращает Saga JWT.

Запрос

POST /api/auth/supabase/login
Authorization: Bearer <supabase_jwt>
Content-Type: application/json

{
  "redirectUrl": "/dashboard"
}

Заголовки:

Заголовок Значение Обязательно Описание
Authorization Bearer <supabase_jwt> ✅ Да Supabase access_token от frontend
Content-Type application/json ✅ Да JSON content type

Схема тела запроса:

Поле Тип Обязательно Описание
redirectUrl string ❌ Нет URL для redirect после успешного входа

Ответ

Успех (200 OK) - Обычный пользователь:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 86400,
    "tokenType": "Bearer",
    "user": {
      "id": "user_123abc",
      "email": "user@example.com",
      "full_name": "John Doe",
      "created_at": "2025-11-13T10:00:00Z"
    },
    "isAdmin": false
  },
  "message": "Authentication successful",
  "timestamp": "2025-11-13T15:30:00Z"
}

Успех (200 OK) - Admin пользователь:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 86400,
    "tokenType": "Bearer",
    "user": {
      "id": "admin_456def",
      "email": "admin@saga-test.com",
      "full_name": "Admin User",
      "created_at": "2025-11-13T10:00:00Z"
    },
    "isAdmin": true,
    "adminPermissions": ["*"]
  },
  "message": "Admin authentication successful",
  "timestamp": "2025-11-13T15:30:00Z"
}

Ошибки:

Код Код ошибки Описание
400 VALIDATION_ERROR Missing Authorization header
401 SUPABASE_JWT_INVALID Invalid Supabase JWT signature
401 EMAIL_MANDATORY Email not found in JWT claims
401 EMAIL_NOT_VERIFIED Email not verified in Supabase
500 USER_CREATION_ERROR Failed to create user in database
500 JWT_GENERATION_ERROR Failed to generate Saga JWT

Пример cURL

# Получение Supabase JWT происходит на frontend через Supabase SDK
# Затем отправляем его к Saga API:

SUPABASE_JWT="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

curl -X POST https://app.saga.surf/api/auth/supabase/login \
  -H "Authorization: Bearer $SUPABASE_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "redirectUrl": "/dashboard"
  }'

Пример TypeScript

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

interface SagaAuthResponse {
  token: string;
  expiresIn: number;
  tokenType: string;
  user: {
    id: string;
    email: string;
    full_name: string;
    created_at: string;
  };
  isAdmin: boolean;
  adminPermissions?: string[];
}

class SagaAuthService {
  async loginWithGoogle(): Promise<SagaAuthResponse> {
    // 1. Supabase OAuth login
    const { error: oauthError } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`
      }
    });

    if (oauthError) throw oauthError;

    // После redirect получаем session
    const { data: session, error: sessionError } = await supabase.auth.getSession();

    if (sessionError || !session?.session) {
      throw new Error('No Supabase session found');
    }

    // 2. Exchange Supabase JWT for Saga JWT
    return this.exchangeSupabaseJWT(session.session.access_token);
  }

  async loginWithEmailPassword(email: string, password: string): Promise<SagaAuthResponse> {
    // 1. Supabase email/password login
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password
    });

    if (error || !data.session) throw error || new Error('Login failed');

    // 2. Exchange Supabase JWT for Saga JWT
    return this.exchangeSupabaseJWT(data.session.access_token);
  }

  private async exchangeSupabaseJWT(supabaseJWT: string): Promise<SagaAuthResponse> {
    const response = await fetch('/api/auth/supabase/login', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${supabaseJWT}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        redirectUrl: '/dashboard'
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Authentication failed');
    }

    const { data } = await response.json();

    // Сохраняем Saga JWT для будущих запросов
    localStorage.setItem('saga_auth_token', data.token);

    return data;
  }
}

// Usage
const authService = new SagaAuthService();

// Google OAuth
try {
  const authData = await authService.loginWithGoogle();
  console.log('Logged in:', authData.user.email);
  console.log('Is Admin:', authData.isAdmin);
} catch (error) {
  console.error('Login failed:', error);
}

// Email/Password
try {
  const authData = await authService.loginWithEmailPassword(
    'user@example.com',
    'password123'
  );
  console.log('Logged in:', authData.user.email);
} catch (error) {
  console.error('Login failed:', error);
}

GET /api/auth/user/profile

Получить профиль аутентифицированного пользователя.

Запрос

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

Заголовки:

Заголовок Значение Обязательно Описание
Authorization Bearer <saga_jwt> ✅ Да Saga JWT токен

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "user_123abc",
    "email": "user@example.com",
    "full_name": "John Doe",
    "created_at": "2025-11-13T10:00:00Z",
    "updated_at": "2025-11-13T10:00:00Z",
    "last_login_at": "2025-11-13T15:30:00Z",
    "status": "active"
  },
  "message": "Profile retrieved successfully",
  "timestamp": "2025-11-13T15:30:00Z"
}

Ошибки:

Код Код ошибки Описание
401 UNAUTHORIZED Missing or invalid Authorization header
401 EMAIL_MANDATORY Email not found in JWT claims
404 USER_NOT_FOUND User not found in database
500 INTERNAL_SERVER_ERROR Error retrieving profile

Пример cURL

SAGA_JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl -X GET https://app.saga.surf/api/auth/user/profile \
  -H "Authorization: Bearer $SAGA_JWT"

Пример TypeScript

interface UserProfile {
  id: string;
  email: string;
  full_name: string;
  created_at: string;
  updated_at: string;
  last_login_at: string;
  status: string;
}

async function getUserProfile(): Promise<UserProfile> {
  const token = localStorage.getItem('saga_auth_token');
  if (!token) throw new Error('Not authenticated');

  const response = await fetch('/api/auth/user/profile', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (!response.ok) {
    throw new Error('Failed to get profile');
  }

  const { data } = await response.json();
  return data;
}

// Usage
try {
  const profile = await getUserProfile();
  console.log('User profile:', profile);
} catch (error) {
  console.error('Failed to load profile:', error);
}

POST /api/auth/logout

Выход из системы с инвалидацией токена.

Запрос

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

Заголовки:

Заголовок Значение Обязательно Описание
Authorization Bearer <saga_jwt> ✅ Да Saga JWT токен для инвалидации

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "message": "Logged out successfully"
  },
  "message": "Session terminated",
  "timestamp": "2025-11-13T15:30:00Z"
}

Ошибки:

Код Код ошибки Описание
401 UNAUTHORIZED Missing or invalid Authorization header
500 INTERNAL_SERVER_ERROR Error during logout

Пример cURL

SAGA_JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl -X POST https://app.saga.surf/api/auth/logout \
  -H "Authorization: Bearer $SAGA_JWT"

Пример TypeScript

async function logout(): Promise<void> {
  const token = localStorage.getItem('saga_auth_token');
  if (!token) return; // Already logged out

  try {
    await fetch('/api/auth/logout', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
  } catch (error) {
    console.error('Logout request failed:', error);
    // Continue with local cleanup anyway
  } finally {
    // Clear local storage and Supabase session
    localStorage.removeItem('saga_auth_token');

    // Clear Supabase session
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
    await supabase.auth.signOut();

    console.log('Logged out successfully');
    window.location.href = '/login';
  }
}

GET /api/auth/status

Проверка статуса аутентификации БЕЗ возврата 401 ошибок. Endpoint ВСЕГДА возвращает 200 OK.

Запрос

GET /api/auth/status
Authorization: Bearer <saga_jwt>

Заголовки (опционально):

Заголовок Значение Описание
Authorization Bearer <saga_jwt> Saga JWT токен (опционально)

Ответ

Всегда 200 OK - authenticated:

{
  "success": true,
  "data": {
    "authenticated": true,
    "user": {
      "id": "user_123abc",
      "email": "user@example.com"
    },
    "tokenValid": true,
    "isAdmin": false
  },
  "message": "User is authenticated",
  "timestamp": "2025-11-13T15:30:00Z"
}

Всегда 200 OK - not authenticated:

{
  "success": true,
  "data": {
    "authenticated": false,
    "reason": "no_token"
  },
  "message": "User not authenticated",
  "timestamp": "2025-11-13T15:30:00Z"
}

Всегда 200 OK - invalid token:

{
  "success": true,
  "data": {
    "authenticated": false,
    "reason": "invalid_token"
  },
  "message": "Invalid authentication token",
  "timestamp": "2025-11-13T15:30:00Z"
}

Пример cURL

# With token
curl -X GET https://app.saga.surf/api/auth/status \
  -H "Authorization: Bearer $SAGA_JWT"

# Without token
curl -X GET https://app.saga.surf/api/auth/status

Пример TypeScript

interface AuthStatusResponse {
  authenticated: boolean;
  user?: {
    id: string;
    email: string;
  };
  tokenValid?: boolean;
  isAdmin?: boolean;
  reason?: string;
}

async function checkAuthStatus(): Promise<AuthStatusResponse> {
  const token = localStorage.getItem('saga_auth_token');
  const headers: HeadersInit = {};

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  const response = await fetch('/api/auth/status', { headers });
  const { data } = await response.json();
  return data;
}

// React Hook for auth status
import { useState, useEffect } from 'react';

function useAuthStatus() {
  const [status, setStatus] = useState<AuthStatusResponse | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkAuthStatus()
      .then(setStatus)
      .finally(() => setLoading(false));
  }, []);

  return { status, loading };
}

// Usage in component
function AuthGuard({ children }: { children: React.ReactNode }) {
  const { status, loading } = useAuthStatus();

  if (loading) return <div>Loading...</div>;
  if (!status?.authenticated) return <div>Please login</div>;

  return <>{children}</>;
}

Admin Authentication Endpoints

POST /api/auth/supabase/login (Admin)

Тот же endpoint что для пользователей, но результат зависит от email в config.yaml. Если email найден в ADMIN_EMAILS, возвращается admin JWT.

Admin Configuration (config/auth.yaml)

auth:
  admin_emails:
    - "admin@saga-test.com"
    - "operator@saga-test.com"
    - "finance@saga-test.com"

Admin Response Example

При входе с admin email:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 86400,
    "user": {
      "id": "admin_456def",
      "email": "admin@saga-test.com",
      "full_name": "Admin User"
    },
    "isAdmin": true,
    "adminPermissions": ["*"]
  },
  "message": "Admin authentication successful"
}

GET /api/auth/admin/profile

Получить admin профиль (требует admin JWT).

Запрос

GET /api/auth/admin/profile
Authorization: Bearer <admin_saga_jwt>

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "admin_456def",
    "email": "admin@saga-test.com",
    "full_name": "Admin User",
    "created_at": "2025-11-13T10:00:00Z",
    "status": "active",
    "adminPermissions": ["*"],
    "role": "super_admin"
  },
  "message": "Admin profile retrieved",
  "timestamp": "2025-11-13T15:30:00Z"
}

Ошибки:

Код Код ошибки Описание
401 UNAUTHORIZED Missing or invalid Authorization header
403 ADMIN_ACCESS_DENIED Token is not admin or email not in config
500 INTERNAL_SERVER_ERROR Error retrieving admin profile

Пример TypeScript

async function getAdminProfile(): Promise<AdminProfile> {
  const token = localStorage.getItem('saga_auth_token');
  if (!token) throw new Error('Not authenticated');

  const response = await fetch('/api/auth/admin/profile', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (!response.ok) {
    throw new Error('Admin access denied');
  }

  const { data } = await response.json();
  return data;
}

GET /api/auth/admin/health

Health check админской аутентификации.

Ответ

Всегда 200 OK:

{
  "success": true,
  "data": {
    "status": "healthy",
    "service": "admin-auth",
    "configuredAdmins": 3,
    "timestamp": "2025-11-13T15:30:00Z"
  },
  "message": "Admin auth service is healthy"
}

Integration Examples

Complete React Authentication Hook

import { useState, useEffect, useContext, createContext, ReactNode } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

interface AuthContextType {
  user: UserProfile | null;
  isAuthenticated: boolean;
  isAdmin: boolean;
  loading: boolean;
  login: (email: string, password: string) => Promise<void>;
  loginWithGoogle: () => Promise<void>;
  logout: () => Promise<void>;
  refresh: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<UserProfile | null>(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isAdmin, setIsAdmin] = useState(false);
  const [loading, setLoading] = useState(true);

  // Check auth status on mount
  useEffect(() => {
    refresh();
  }, []);

  async function login(email: string, password: string) {
    setLoading(true);
    try {
      // 1. Supabase login
      const { data, error } = await supabase.auth.signInWithPassword({
        email,
        password
      });

      if (error || !data.session) throw error || new Error('Login failed');

      // 2. Exchange for Saga JWT
      await exchangeSupabaseJWT(data.session.access_token);
    } catch (error) {
      console.error('Login failed:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  }

  async function loginWithGoogle() {
    setLoading(true);
    try {
      const { error } = await supabase.auth.signInWithOAuth({
        provider: 'google',
        options: {
          redirectTo: `${window.location.origin}/auth/callback`
        }
      });

      if (error) throw error;
      // OAuth completion handled in callback page
    } catch (error) {
      setLoading(false);
      throw error;
    }
  }

  async function exchangeSupabaseJWT(supabaseJWT: string) {
    const response = await fetch('/api/auth/supabase/login', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${supabaseJWT}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({})
    });

    if (!response.ok) {
      throw new Error('Saga authentication failed');
    }

    const { data } = await response.json();

    // Store Saga JWT
    localStorage.setItem('saga_auth_token', data.token);

    // Update state
    setUser(data.user);
    setIsAuthenticated(true);
    setIsAdmin(data.isAdmin || false);
  }

  async function logout() {
    try {
      // 1. Logout from Saga
      const token = localStorage.getItem('saga_auth_token');
      if (token) {
        await fetch('/api/auth/logout', {
          method: 'POST',
          headers: { 'Authorization': `Bearer ${token}` }
        });
      }

      // 2. Logout from Supabase
      await supabase.auth.signOut();

      // 3. Clear state
      localStorage.removeItem('saga_auth_token');
      setUser(null);
      setIsAuthenticated(false);
      setIsAdmin(false);
    } catch (error) {
      console.error('Logout error:', error);
    }
  }

  async function refresh() {
    setLoading(true);
    try {
      const status = await checkAuthStatus();
      if (status.authenticated && status.user) {
        setUser(status.user as UserProfile);
        setIsAuthenticated(true);
        setIsAdmin(status.isAdmin || false);
      } else {
        setUser(null);
        setIsAuthenticated(false);
        setIsAdmin(false);
      }
    } catch (error) {
      console.error('Auth refresh failed:', error);
      setUser(null);
      setIsAuthenticated(false);
      setIsAdmin(false);
    } finally {
      setLoading(false);
    }
  }

  return (
    <AuthContext.Provider value={{
      user,
      isAuthenticated,
      isAdmin,
      loading,
      login,
      loginWithGoogle,
      logout,
      refresh
    }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

OAuth Callback Handler

// pages/auth/callback.tsx (Next.js)
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '../../../hooks/useAuth';

export default function AuthCallback() {
  const router = useRouter();
  const { refresh } = useAuth();

  useEffect(() => {
    async function handleOAuthCallback() {
      try {
        // Supabase automatically handles OAuth callback
        await refresh();

        // Redirect to dashboard
        router.push('/dashboard');
      } catch (error) {
        console.error('OAuth callback failed:', error);
        router.push('/login?error=oauth_failed');
      }
    }

    handleOAuthCallback();
  }, [router, refresh]);

  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="text-center">
        <div className="spinner" />
        <p>Completing authentication...</p>
      </div>
    </div>
  );
}

API Request Interceptor

import axios from 'axios';

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL
});

// Request interceptor - add auth token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('saga_auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - handle auth errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token expired or invalid
      localStorage.removeItem('saga_auth_token');
      window.location.href = '/login?expired=true';
    }
    return Promise.reject(error);
  }
);

export default api;

Security Considerations (Integration-Only)

Email Security

Email Mandatory Enforcement:

  • ✅ ВСЕГДА проверяйте email presence в JWT claims
  • ✅ Используйте email как PRIMARY IDENTIFIER во всех операциях
  • ✅ Верифицируйте email через Supabase (email_verified claim)
  • ❌ НИКОГДА не создавайте пользователя без email

Supabase JWT Verification

JWKS-Based Security:

  • ✅ Используйте Supabase JWKS endpoint для верификации подписи
  • ✅ Проверяйте issuer (iss) claim = ваш Supabase project URL
  • ✅ Проверяйте audience (aud) claim = "authenticated"
  • ✅ Проверяйте expiration (exp) claim

Admin Security

Config-Based Protection:

  • ✅ Админы ТОЛЬКО в config.yaml (НЕ в БД)
  • ✅ Double-check: JWT claims + config verification
  • ✅ Логируйте все admin access attempts
  • ❌ НИКОГДА не храните admin статус в database

Session Management

Token Lifecycle:

  • ✅ Saga JWT expires in 24 hours
  • ✅ Supabase refresh token handles automatic renewal
  • ✅ Store tokens securely (httpOnly cookies preferred)
  • ✅ Clear all tokens on logout

Error Handling Patterns

Standard Error Response

{
  "success": false,
  "error": {
    "code": "EMAIL_MANDATORY",
    "message": "Email is required for all authentication operations",
    "details": {
      "field": "email",
      "reason": "missing_from_jwt_claims"
    }
  },
  "timestamp": "2025-11-13T15:30:00Z"
}

Common Error Codes

Код HTTP Status Описание Решение
SUPABASE_JWT_INVALID 401 Invalid Supabase JWT signature Refresh Supabase session
EMAIL_MANDATORY 401 Email not found in JWT Check Supabase OAuth settings
EMAIL_NOT_VERIFIED 401 Email not verified Complete email verification
ADMIN_ACCESS_DENIED 403 Email not in admin config Check config/auth.yaml
USER_CREATION_ERROR 500 Database error during user creation Contact support

Frontend Error Handling

async function handleAuthError(error: any) {
  switch (error.code) {
    case 'SUPABASE_JWT_INVALID':
      // Refresh Supabase session
      await supabase.auth.refreshSession();
      break;

    case 'EMAIL_NOT_VERIFIED':
      // Show email verification prompt
      showEmailVerificationModal();
      break;

    case 'ADMIN_ACCESS_DENIED':
      // Redirect to user dashboard
      router.push('/dashboard');
      break;

    default:
      // Generic error handling
      showErrorToast(error.message);
  }
}

Authentication Guides:

Architecture:

Other Endpoints:


✍️ Document Information

Author: Saga Development Team Contributors: Backend Engineer, Security Specialist, Frontend Architect Architecture: Integration-Only (Email-First Mandatory, No Web3 Complexity)


"Authentication should be invisible to users, bulletproof for developers, and maintainable for teams."

— Saga Engineering Team (Integration-Only Principles)


📋 Метаданные

Версия: 2.6.268 Обновлено: 2025-11-13 Статус: Ready for Implementation Архитектура: Email-First, Supabase Auth, Config-Based Admins