Содержание Показать Скрыть
- Три модели в одном предложении каждая
- Анатомия: что внутри токена
- JWT (JSON Web Token)
- OAuth 2.0 Access Token
- Me2eM Session
- Кто подписывает: сервер vs клиент
- JWT: сервер подписывает
- Me2eM: клиент подписывает
- OAuth: промежуточный вариант
- Сравнительная таблица
- Модель доверия: кто кому верит
- JWT — доверие серверу целиком
- OAuth — доверие трём сторонам
- Me2eM — доверие криптографии
- Утечка: что происходит
- JWT — утечка серверного секрета
- Me2eM — утечка ключа хэндла
- Me2eM — утечка seed
- Код: один сценарий, три реализации
- JWT
- OAuth 2.0
- Me2eM
- Сводная таблица
- Когда что выбирать
- Что читать дальше
JWT, OAuth 2.0 и Me2eM решают одну задачу: доказать серверу, что клиент имеет право на действие. Все три выдают токен, все три проверяют его на каждом запросе, все три обещают безопасность.
Но за поверхностным сходством — три фундаментально разные модели доверия. И выбор между ними — не выбор технологии, а выбор кто владеет ключом и кто может ошибиться безвозвратно.
Три модели в одном предложении каждая
JWT — сервер подписывает токен своим секретом или ключом. Сервер всемогущ: выдаёт, отзывает, интерпретирует. Клиент получает готовое свидетельство.
OAuth 2.0 — клиент делегирует доступ к чужим ресурсам через авторизационный сервер. Сервер-владелец ресурса решает, что разрешить. Клиент действует в рамках делегированного.
Me2eM — пользователь владеет seed-фразой, из которой детерминированно выводятся все ключи. Клиент сам подписывает свои токены, а сервер только верифицирует их — напрямую (Mode 1) или против аттестационной цепочки (Mode 2). Никто не владеет правом создавать чужие токены.
Разница не косметическая. Это три разных ответа на один вопрос:
Кому вы доверяете право создать токен с полномочиями?
Анатомия: что внутри токена
Начнём с структуры — она уже показывает философию.
JWT (JSON Web Token)
// Стандартная структура JWT:
// header.payload.signature (все три — base64url)
// Header:
{ "alg": "HS256", "typ": "JWT" }
// Payload:
{
"sub": "user-12345", // subject — ID из БД
"name": "Alice",
"role": "admin", // права — вписаны сервером
"iat": 1716912000,
"exp": 1716915600
}
// Signature: HMAC-SHA256(header, payload, serverSecret)
// Или RSA/ECDSA — подпись серверным приватным ключом
OAuth 2.0 Access Token
// OAuth-токен — обычно opaque (непрозрачный):
// auth-сервер выдаёт случайную строку и хранит её значение у себя
{
"access_token": "at-9f2XkQ7Lm4Vn8pR3tW6yZ1cB5dE8gH2j" // случайная строка;
// значение живёт в БД auth-сервера
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200..."
}
// Если JWT-as-access-token:
// структура та же, что выше — но выдан через OAuth flow
Me2eM Session
// Me2em session payload:
{
"hId": "base64url-публичный-ключ-хэндла",
"hNm": "work",
// "hPath": ["work", "laptop-1"], // только у SubHandle: путь в дереве
"aud": "app.example.com", // для кого
"scp": ["read", "write"], // права — заявлены клиентом,
// проверены сервером
"exp": 1716915600,
"iat": 1716912000,
"jti": "уникальный-ID"
}
// Signature: Ed25519(ключ хэндла, payload)
// Подпись — ключом ХЭНДЛА, не серверным секретом
Ключевое различие на уровне структуры: в JWT sub — ссылка на БД (нужен lookup, чтобы понять, кто это). В OAuth access_token — обычно opaque (нужен introspection endpoint). В Me2eM hId — сам публичный ключ, верифицируемый криптографически без lookup’а.
Кто подписывает: сервер vs клиент
Это — главная развилка.
JWT: сервер подписывает
1. Пользователь логинится (пароль, OAuth-провайдер, ...)
2. Сервер создаёт payload { sub, role, exp, ... }
3. Сервер подписывает payload СВОИМ секретом
4. Клиент получает JWT и передаёт его при каждом запросе
5. Сервер проверяет подпись СВОИМ секретом
Сервер — единственный источник токенов. Он может создать токен для любого пользователя с любыми правами. Пользователь не может создать токен сам — и в этом сила модели (сервер контролирует выдачу), и её ограничение (сервер нужен онлайн, сервер — единственная точка компрометации).
Me2eM: клиент подписывает
1. Пользователь вводит seed-фразу (один раз за сессию)
2. Детерминированно вычисляется Handle: приватный ключ
3. Клиент создаёт payload { hId, aud, scp, exp, ... }
4. Клиент ПОДПИСЫВАЕТ payload ключом ХЭНДЛА
5. Сервер проверяет подпись против публичного ключа ХЭНДЛА
(пересчитанного из имени в токене)
Сервер не может создать токен для пользователя — у него нет его приватного ключа. Пользователь не может создать токен с расширенными правами — payload подписан, изменить поля без инвалидации подписи невозможно. Права заявлены клиентом и закреплены подписью, а в Mode 2 верификатор дополнительно принуждает их к грантам аттестаций при каждой проверке.
Одна честная оговорка про Mode 1. Всё сказанное «сервер не может» — безусловно верно для Mode 2, где у сервера только публичный корень. В Mode 1 верификатор держит
Identity— корень — и, значит, может вывести любой ключ дерева. Именно поэтому Mode 1 предназначен только для инфраструктуры, которой вы и так доверяете выдачу прав; для всех внешних аудиторий — Mode 2.
OAuth: промежуточный вариант
OAuth 2.0 не подписывает сам — он делегирует. Клиент получает токен от авторизационного сервера (authorization code flow, client credentials и т.д.) и предъявляет его ресурсному серверу. Токен может быть opaque (introspection) или JWT (self-contained).
Сложность OAuth — в количестве сущностей: авторизационный сервер, ресурсный сервер, клиент, владелец ресурсов, scope’ы, redirect URI’ы, refresh-токены. Это цена гибкости для сценариев «приложение A хочет доступ к данным пользователя на сервисе B».
Сравнительная таблица
| Аспект | JWT | OAuth 2.0 | Me2eM Session |
|---|---|---|---|
| Кто подписывает | Сервер | Авторизационный сервер | Клиент (хэндл) |
| Основа подписи | Серверный секрет / ключ | N/A (opaque) или серверный ключ | Детерминированный ключ хэндла |
| Хранение сессий на сервере | Нет (self-contained) | Да (introspection / reference token) | Нет (stateless) |
| Отзыв | Нет встроенного (нужен БД-чёрный список) | Да (refresh flow, introspection) | Да (jti + RevocationChecker) |
| Идентичность | Строка sub из БД | Строка из auth-сервера | Публичный ключ — верифицируемый |
| Иерархия | Нет | Нет | Да (hPath: Handle → SubHandle) |
| Кто может подделать токен | Тот, кто знает серверный секрет | Auth-сервер; украденный токен — использование, а не подделка | Тот, кто знает приватный ключ хэндла |
| Пересчёт pubkey | Не нужен | Не нужен | Да — по имени из токена (Mode 1) |
| Анонимность | Низкая (sub — идентификатор в БД) | Низкая (клиент идентифицируем) | Высокая (нет связи с email/телефоном) |
Модель доверия: кто кому верит
JWT — доверие серверу целиком
Пользователь → доверяет → Сервер → подписывает → Токен
↓
Сервер может:
- выдать любые права
- отозвать любую сессию
- читать payload (он не зашифрован)
- утратить секрет → все токены скомпрометированы
OAuth — доверие трём сторонам
Пользователь → доверяет → Auth-серверу → выдаёт токен
→ доверяет → Ресурс-серверу → проверяет токен
→ доверяет → Клиенту → не превысит scope
Me2eM — доверие криптографии
Пользователь → владеет → Seed-фразой → детерминирует → все ключи
Клиент → подписывает → токен → предъявляет
Сервер → верифицирует → подпись + аттестации → публичным корнем (Mode 2)
← сервер НЕ может создать чужой токен — только проверить
← содержимое payload открыт, но подделать его нельзя
Утечка: что происходит
Ключевой вопрос при выборе модели: что происходит при компрометации.
JWT — утечка серверного секрета
Секрет утёк → атакующий может выдать ТОКЕН ДЛЯ ЛЮБОГО ПОЛЬЗОВАТЕЛЯ
→ с любыми правами, любым exp, любым sub
→ до ротации секрета
→ без следа (нет логов создания)
Mitigation: ротация секрета → все JWT становятся невалидными → все
пользователи разлогинены
Me2eM — утечка ключа хэндла
Приватный ключ хэндла утёк → атакующий может подписать сессии ЭТОГО
хэндла (в рамках его прав)
Mitigation 1: отзыв аттестации хэндла (Mode 2) — мгновенно гасит
текущие и будущие сессии во всех сервисах
Mitigation 2: короткий TTL (рекомендация — до часа) — окно атаки
ограничено
Риск ограничен ХЭНДЛОМ — другие хэндлы того же пользователя не затронуты
Me2eM — утечка seed
Seed утёк → атакующий выводит ВСЁ ДЕРЕВО: все хэндлы, все ключи
→ полная компрометация идентичности
→ ротации нет: детерминизм всегда воспроизведёт те же ключи
Mitigation: seed только на бумаге, никогда на устройстве и в облаке;
passphrase («25-е слово») — вторая степень свободы: фраза
без кодового слова даёт другую (пустую для атакующего)
идентичность
Код: один сценарий, три реализации
Сценарий: пользователь хочет получить доступ к API. Смотрим, как выглядит каждый подход.
JWT
// Сервер:
app.post('/login', async (req, res) => {
const user = await verifyPassword(req.body.email, req.body.password);
if (!user) return res.status(401).send('Invalid credentials');
const token = jwt.sign(
{ sub: user.id, role: user.role },
SERVER_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
// Проверка на каждом запросе:
function authenticate(req) {
const payload = jwt.verify(req.headers.authorization, SERVER_SECRET);
// Look up user in DB to confirm role still valid
const user = await db.users.findById(payload.sub);
return user;
}
OAuth 2.0
// Шаг 1: redirect на auth-сервер
res.redirect(`https://auth.example.com/authorize?` +
`response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=read`);
// Шаг 2: обмен code на token
const token = await fetch('https://auth.example.com/token', {
method: 'POST',
body: `grant_type=authorization_code&code=${code}&client_id=${CLIENT_ID}`
});
// Шаг 3: каждый запрос — introspection или local JWT verify
const introspect = await fetch('https://auth.example.com/introspect', {
method: 'POST',
body: `token=${token}`,
headers: { 'Authorization': `Basic ${CLIENT_CREDENTIALS}` }
});
const { active, scope, sub } = await introspect.json();
Me2eM
import { Identity, Session } from '@me2em/core';
// Единственный вход — seed-фраза (на бумаге). Регистрации нет:
// все хэндлы уже существуют, они детерминированы.
const identity = await Identity.fromSeed(seedBytes);
const handle = await identity.deriveHandle('work');
// Создание сессии (на клиенте, без сервера):
const session = await Session.create(handle, {
audience: 'app.example.com',
scopes: ['read', 'write'],
ttl: 3600,
});
// Верификация (на сервере, без БД):
const verified = await Session.verifyStateless(
session.token,
serverIdentity, // Identity — пересчитывает pubkey хэндла
'app.example.com', // RevocationChecker — опциональный 4-й аргумент
);
// verified.scopes — права из токена
// verified.handleName — кто именно
Уточнение о двух режимах. Пример выше — Mode 1 (verifyStateless): верификатор держит Identity и сам пересчитывает ключи по именам из токена. Для внешних аудиторий существует Mode 2 (verifyAttested): сервер проверяет цепочку родительских аттестаций против публичного корня — тогда серверного секрета нет вовсе, а гранты принуждаются при каждой проверке. Строки таблиц, где у Me2eM «нет серверного секрета», относятся именно к Mode 2; полный разбор — в статье об аттестационных цепочках.
Сводная таблица
| Критерий | JWT | OAuth 2.0 | Me2eM Session |
|---|---|---|---|
| Кто создаёт токен | Сервер | Auth-сервер | Клиент (хэндл) |
| Серверный секрет | Нужен | Нужен | Mode 1: верификатор держит корень · Mode 2: не нужен |
| Stateless | Да (self-contained JWT) | Обычно нет (introspection) | Да |
| Отзыв | Нет встроенного | Да (introspection) | Да (jti + RevocationChecker; ветвевой — в Mode 2) |
| Иерархия прав | Нет | Нет (плоские scope-строки) | Да (attestation chains) |
| Идентичность | Строка sub из БД | Строка sub | Публичный ключ |
| Анонимность | Низкая | Низкая | Высокая |
| Утечка секрета сервера | Критическая | Критическая | Mode 1: критическая (это корень) · Mode 2: N/A |
| Утечка клиентского ключа | N/A (клиент не хранит ключ) | N/A | Ограничена (один хэндл, короткий TTL, ревокация в Mode 2) |
| Мультидевайсность | Требует sync | Требует sync | Детерминированная (тот же seed) |
| Регистрация | Email/телефон | Email/телефон | Только seed-фраза |
| Делегирование | Ограничено | Основная цель | Attestation chains |
Когда что выбирать
| Ситуация | Рекомендация |
|---|---|
| Обычный web-app с БД пользователей | JWT — отработанный стандарт |
| Мульти-сервисная архитектура с делегированием | OAuth 2.0 — отработанный стандарт |
| Криптографическая идентичность без БД | Me2eM Session |
| E2EE-коммуникация | Me2eM + @me2em/crypto |
| IoT-устройства с офлайн-авторизацией | Me2eM + attestation chains |
| AI-агенты с детерминированными ключами | Me2eM + attestation chains |
| Анонимные пользователи (без email/телефона) | Me2eM |
| Обычный SaaS без криптографических требований | JWT / OAuth — не переусложняйте |
Me2eM не заменяет JWT или OAuth — он решает другую задачу. Если вам нужна анонимная, детерминированная, делегируемая идентичность без серверных секретов и без БД — Me2eM делает это нативно. Если вам нужна обычная авторизация в web-приложении с БД пользователей — JWT или OAuth достаточно и проще.
Что читать дальше
- SSO без базы пользователей — предельный случай: seed-фраза как аккаунт всей платформы
- Stateless-сессии: авторизация без базы данных — что внутри токена и какие семь проверок проходит сессия
- Аттестационные цепочки: делегирование доверия без сервера — Mode 2: делегирование и ревокация веток
- Мгновенный отзыв: ревокация в stateless-мире — как устроен отзыв jti
- Me2eM: идентичность из seed-фразы — фундамент
@me2em/coreна npm — реализация