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¶
Получить профиль аутентифицированного пользователя.
Запрос¶
Заголовки:
| Заголовок | Значение | Обязательно | Описание |
|---|---|---|---|
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¶
Выход из системы с инвалидацией токена.
Запрос¶
Заголовки:
| Заголовок | Значение | Обязательно | Описание |
|---|---|---|---|
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.
Запрос¶
Заголовки (опционально):
| Заголовок | Значение | Описание |
|---|---|---|
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)¶
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).
Запрос¶
Ответ¶
Успех (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);
}
}
Related Documentation¶
Authentication Guides:
- Email-First Authentication Flow - Complete Integration-Only auth guide
- Supabase Integration - Supabase setup and configuration
Architecture:
- Integration-Only Architecture - Core principles
- Database Schema - Email mandatory schema
Other Endpoints:
- User Endpoints - User management operations
- Admin Endpoints - Admin-specific operations
✍️ 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