Данный документ описывает процесс интеграции аутентификации на фронтенде (web/mobile) с бэкендом платформы.
- Общая схема аутентификации
- Аутентификация агента (для SSO эндпоинтов)
- Шаг 1: Аутентификация через Privy SDK
- Шаг 2: Обмен Privy токена на JWT платформы
- Авторизация запросов к API
- Обновление токена
- Выход из системы
- Хранение токенов и cookies
- Коды ошибок
- Примеры кода
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Privy Service │ │ Backend │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ 1. Login (email/wallet/social) │
│──────────────────────>│ │
│ │ │
│ 2. Privy Access Token│ │
│<──────────────────────│ │
│ │ │
│ 3. POST /sso/exchange-privy-token │
│───────────────────────────────────────────────>│
│ { privy_token, agent_id } │
│ │ │
│ 4. { access_token, refresh_token, user } │
│<───────────────────────────────────────────────│
│ + Set-Cookie: client_refresh_token │
│ │ │
│ 5. API запросы с JWT │
│───────────────────────────────────────────────>│
│ Authorization: Bearer <access_token> │
│ X-Auth-Type: jwt │
│ │ │
Все /sso/* эндпоинты требуют идентификации агента. Существует два метода в зависимости от эндпоинта:
Следующие эндпоинты поддерживают Basic Auth для идентификации агента:
POST /sso/refresh-tokenPOST /sso/logoutPOST /sso/init-email-signinPOST /sso/confirm-email-signinPOST /sso/init-wallet-signinPOST /sso/confirm-wallet-signin
Authorization: Basic base64(agent_name:agent_secret)
Если ваши учётные данные агента:
- Имя агента:
my_mobile_app - Секрет агента:
supersecret123
Закодируйте my_mobile_app:supersecret123 в Base64:
Authorization: Basic bXlfbW9iaWxlX2FwcDpzdXBlcnNlY3JldDEyMw==
POST /sso/refresh-token HTTP/1.1
Host: api.example.com
Authorization: Basic bXlfbW9iaWxlX2FwcDpzdXBlcnNlY3JldDEyMw==
Content-Type: application/json
{
"refresh_token": "abc123..."
}Примечание: Если заголовок
Authorizationне предоставлен, бэкенд будет использовать агента по умолчанию (ID=1), который предназначен для основного веб-приложения платформы. Для сторонних интеграций необходимо предоставить учётные данные агента.
Эндпоинт POST /sso/exchange-privy-token использует другой подход:
- Агент идентифицируется через поле
agent_idв теле запроса - Заголовок Origin проверяется на соответствие списку разрешённых origins агента
- Заголовок Basic Auth не требуется
См. Шаг 2: Обмен Privy токена на JWT платформы для подробностей.
Обратитесь к администратору бэкенда для получения:
agent_id- уникальный числовой идентификатор вашего приложенияagent_name- имя агента для Basic Authagent_secret- секрет агента для Basic Auth- Убедитесь, что домен вашего приложения добавлен в разрешённые origins агента
Первым шагом необходимо интегрировать Privy SDK в ваше приложение. Privy поддерживает множество методов аутентификации:
- SMS
- Кошельки (MetaMask, Phantom и др.)
- Социальные сети (Google, Twitter и др.)
После успешной аутентификации через Privy вы получите Privy Access Token.
POST /sso/exchange-privy-token
{
"privy_token": "string", // Privy Access Token, полученный после аутентификации
"agent_id": 1 // ID агента (приложения). Получите у администратора.
}{
"user": {
"ID": 123,
"CreatedAt": "2024-01-15T10:00:00Z",
"UpdatedAt": "2024-01-15T10:00:00Z",
"DeletedAt": null,
"Email": "user@example.com",
"ImageURL": "https://...",
"ImageThumbnailURL": "https://...",
"Name": "User Name",
"IsEmailVerified": true,
"BalanceUSD": 100.50,
"EthereumAddress": "0x...",
"SolanaAddress": "..."
},
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "abc123..."
}- Origin validation: Бэкенд проверяет заголовок
Originзапроса. Убедитесь, что домен вашего приложения добавлен в список разрешённых для вашегоagent_id. - Cookies: Помимо ответа в JSON, сервер также устанавливает HTTP-only cookie
client_refresh_tokenс refresh токеном. Это запасной вариант для обновления токена.
Для авторизованных запросов к API необходимо передавать два заголовка:
| Заголовок | Значение | Описание |
|---|---|---|
Authorization |
Bearer <access_token> |
JWT токен доступа |
X-Auth-Type |
jwt |
Тип аутентификации |
GET /tokens HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
X-Auth-Type: jwt
Content-Type: application/json- Публичные эндпоинты (не требуют авторизации): получение списка токенов, игр, свечей, метрик
- Защищённые эндпоинты (требуют авторизации): создание токенов, покупка/продажа, голосования, отправка сообщений в чат
Когда access_token истекает, необходимо его обновить.
POST /sso/refresh-token
| Заголовок | Значение | Обязателен |
|---|---|---|
Authorization |
Basic base64(agent_name:agent_secret) |
Да (для сторонних приложений) |
Content-Type |
application/json |
Да |
Примечание: Если заголовок
Authorizationне предоставлен, используется агент по умолчанию (основная платформа).
POST /sso/refresh-token HTTP/1.1
Host: api.example.com
Authorization: Basic bXlfbW9iaWxlX2FwcDpzdXBlcnNlY3JldDEyMw==
Content-Type: application/json
{
"refresh_token": "abc123..."
}{
"refresh_token": "abc123..." // Опционально, если не передан - будет взят из cookie
}Примечание: Refresh token можно передать либо в теле запроса, либо он будет автоматически извлечён из cookie client_refresh_token (если cookies отправляются с запросом).
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "xyz789..."
}- При каждом обновлении выдаётся новый refresh_token. Используйте его для следующего обновления.
- Cookie
client_refresh_tokenтакже обновляется.
Рекомендуется обновлять токен при получении ошибки:
401 Unauthorizedс кодом2(authorization failed)
Или проактивно за несколько минут до истечения (если вы декодируете JWT и проверяете exp claim).
POST /sso/logout
| Заголовок | Значение | Обязателен |
|---|---|---|
Authorization |
Basic base64(agent_name:agent_secret) |
Да (для сторонних приложений) |
Content-Type |
application/json |
Да |
Примечание: Если заголовок
Authorizationне предоставлен, используется агент по умолчанию (основная платформа).
POST /sso/logout HTTP/1.1
Host: api.example.com
Authorization: Basic bXlfbW9iaWxlX2FwcDpzdXBlcnNlY3JldDEyMw==
Content-Type: application/json
{
"refresh_token": "abc123..."
}{
"refresh_token": "abc123..." // Опционально, если не передан - будет взят из cookie
}{
"message": "Logged out successfully"
}- Refresh token инвалидируется на сервере
- Cookie
client_refresh_tokenудаляется (MaxAge=-1)
Бэкенд устанавливает HTTP-only cookie для хранения refresh token:
| Параметр | Значение | Описание |
|---|---|---|
Name |
client_refresh_token |
Имя cookie |
HttpOnly |
true |
Недоступна из JavaScript (защита от XSS) |
Secure |
true (prod) / false (local) |
Только HTTPS в production |
SameSite |
None |
Разрешает cross-domain запросы |
MaxAge |
7 дней | Время жизни cookie |
Path |
/ |
Доступна для всех путей |
| Токен | Где хранить | Примечание |
|---|---|---|
access_token |
В памяти (state/store) | Короткий срок жизни, не сохранять в localStorage |
refresh_token |
В secure storage / cookie | Cookie устанавливается автоматически сервером |
- Access token: Храните в памяти приложения или в secure storage (Keychain на iOS, Keystore на Android)
- Refresh token: Храните в secure storage. Передавайте в теле запроса при обновлении, т.к. работа с cookies может быть сложнее в mobile SDK.
{
"Code": 2,
"Error": "authorization failed"
}| Code | Error | HTTP Status | Описание | Действие |
|---|---|---|---|---|
| 2 | authorization failed | 401 | Токен недействителен или отсутствует | Обновить токен или повторить вход |
| 18 | privy token has expired | 401 | Privy токен истёк | Переаутентифицироваться через Privy |
| 19 | privy token is invalid | 401 | Privy токен недействителен | Переаутентифицироваться через Privy |
| 6 | refresh token not found | 401 | Refresh token не найден | Повторить вход |
| 7 | refresh token is invalid | 401 | Refresh token недействителен | Повторить вход |
| 8 | refresh token has expired | 401 | Refresh token истёк | Повторить вход |
Получена ошибка 401?
├── Code = 2, 7, 8 → Попробовать refresh token
│ ├── Успех → Повторить оригинальный запрос
│ └── Ошибка → Редирект на логин
├── Code = 6 → Редирект на логин
└── Code = 18, 19 → Повторить аутентификацию через Privy
// auth-service.ts
import * as SecureStore from 'expo-secure-store';
const API_BASE_URL = 'https://api.example.com';
const AGENT_ID = 1; // Ваш agent_id
// Учётные данные агента для Basic Auth (получите у администратора бэкенда)
const AGENT_NAME = 'my_mobile_app';
const AGENT_SECRET = 'supersecret123';
// Генерация заголовка Basic Auth
function getBasicAuthHeader(): string {
const credentials = `${AGENT_NAME}:${AGENT_SECRET}`;
const encoded = btoa(credentials); // или используйте библиотеку base-64 для React Native
return `Basic ${encoded}`;
}
interface AuthTokens {
accessToken: string;
refreshToken: string;
}
interface User {
ID: number;
Email: string;
Name: string;
// ... другие поля
}
interface ExchangeTokenResponse {
user: User;
access_token: string;
refresh_token: string;
}
// Хранение токенов
let accessToken: string | null = null;
async function saveRefreshToken(token: string): Promise<void> {
await SecureStore.setItemAsync('refresh_token', token);
}
async function getRefreshToken(): Promise<string | null> {
return await SecureStore.getItemAsync('refresh_token');
}
async function clearTokens(): Promise<void> {
accessToken = null;
await SecureStore.deleteItemAsync('refresh_token');
}
// Обмен Privy токена
export async function exchangePrivyToken(privyToken: string): Promise<{ user: User; tokens: AuthTokens }> {
const response = await fetch(`${API_BASE_URL}/sso/exchange-privy-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
privy_token: privyToken,
agent_id: AGENT_ID,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.Error || 'Token exchange failed');
}
const data: ExchangeTokenResponse = await response.json();
// Сохраняем токены
accessToken = data.access_token;
await saveRefreshToken(data.refresh_token);
return {
user: data.user,
tokens: {
accessToken: data.access_token,
refreshToken: data.refresh_token,
},
};
}
// Обновление токена
export async function refreshAccessToken(): Promise<AuthTokens> {
const refreshToken = await getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${API_BASE_URL}/sso/refresh-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': getBasicAuthHeader(), // Аутентификация агента
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
if (!response.ok) {
const error = await response.json();
// Если refresh token недействителен - очищаем всё
if ([6, 7, 8].includes(error.Code)) {
await clearTokens();
}
throw new Error(error.Error || 'Token refresh failed');
}
const data = await response.json();
accessToken = data.access_token;
await saveRefreshToken(data.refresh_token);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
};
}
// Выход из системы
export async function logout(): Promise<void> {
const refreshToken = await getRefreshToken();
if (refreshToken) {
try {
await fetch(`${API_BASE_URL}/sso/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': getBasicAuthHeader(), // Аутентификация агента
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
} catch {
// Игнорируем ошибки при logout
}
}
await clearTokens();
}
// Авторизованный запрос с автоматическим обновлением токена
export async function authorizedFetch(
url: string,
options: RequestInit = {}
): Promise<Response> {
const makeRequest = async (token: string) => {
const headers = {
...options.headers,
'Authorization': `Bearer ${token}`,
'X-Auth-Type': 'jwt',
'Content-Type': 'application/json',
};
return fetch(`${API_BASE_URL}${url}`, {
...options,
headers,
});
};
if (!accessToken) {
throw new Error('Not authenticated');
}
let response = await makeRequest(accessToken);
// Если 401 - пробуем обновить токен
if (response.status === 401) {
try {
const tokens = await refreshAccessToken();
response = await makeRequest(tokens.accessToken);
} catch {
// Refresh не удался - нужен повторный вход
throw new Error('Session expired');
}
}
return response;
}import { usePrivy } from '@privy-io/react-auth';
import { exchangePrivyToken, authorizedFetch, logout } from './auth-service';
// После успешной аутентификации через Privy
async function handlePrivyAuth() {
const { getAccessToken } = usePrivy();
// Получаем Privy токен
const privyToken = await getAccessToken();
// Обмениваем на токены платформы
const { user, tokens } = await exchangePrivyToken(privyToken);
console.log('Authenticated user:', user);
}
// Пример API запроса
async function createToken(tokenData: any) {
const response = await authorizedFetch('/tokens', {
method: 'POST',
body: JSON.stringify(tokenData),
});
if (!response.ok) {
throw new Error('Failed to create token');
}
return response.json();
}
// Выход
async function handleLogout() {
await logout();
// Перенаправить на экран входа
}- Интегрировать Privy SDK
- Получить учётные данные агента от администратора бэкенда:
-
agent_id- для эндпоинта обмена токенов -
agent_nameиagent_secret- для Basic Auth на других SSO эндпоинтах
-
- Убедиться, что домен приложения добавлен в allowed origins для агента
- Реализовать обмен Privy токена на JWT платформы
- Настроить хранение токенов (secure storage для mobile)
- Добавить заголовок
Authorization: Basic ...к запросам/sso/refresh-tokenи/sso/logout - Реализовать автоматическое обновление токена при 401 ошибке
- Реализовать функцию logout
- Добавить заголовки
Authorization: Bearer ...иX-Auth-Type: jwtко всем защищённым API запросам
Q: Сколько живёт access_token?
A: Срок жизни access token определяется настройками бэкенда. Рекомендуется обрабатывать 401 ошибку и обновлять токен по необходимости.
Q: Сколько живёт refresh_token?
A: 7 дней. После истечения пользователю нужно повторно войти через Privy.
Q: Что делать если и access и refresh токены истекли?
A: Перенаправить пользователя на экран входа и начать процесс аутентификации через Privy заново.
Q: Нужно ли передавать cookies в мобильном приложении?
A: Нет, для мобильных приложений рекомендуется передавать refresh_token в теле запроса, а не полагаться на cookies.
Q: Где получить agent_id?
A: Обратитесь к администратору бэкенда. Каждому приложению/домену выдаётся уникальный agent_id.
Q: В чём разница между Basic Auth и Bearer Auth?
A:
- Basic Auth (
Authorization: Basic ...) используется для идентификации агента на SSO эндпоинтах (/sso/refresh-token,/sso/logout). Он идентифицирует ваше приложение/клиент. - Bearer Auth (
Authorization: Bearer ...) используется для аутентификации пользователя на защищённых API эндпоинтах. Он идентифицирует авторизованного пользователя.
Q: Нужны ли мне и agent_name/agent_secret, И agent_id?
A: Да. agent_id используется в теле запроса /sso/exchange-privy-token, тогда как agent_name и agent_secret используются для Basic Auth на других SSO эндпоинтах (refresh и logout).