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

Me2eM vs JWT vs OAuth: три модели доверия

JWT, OAuth и Me2eM решают одну задачу — авторизация. Но модели доверия фундаментально разные: кто подписывает, кому верит сервер и что происходит при компрометации. Полное сравнение с примерами кода.

M

Me2eM Organization

6 мин. чтения

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».

Сравнительная таблица

АспектJWTOAuth 2.0Me2eM 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; полный разбор — в статье об аттестационных цепочках.

Сводная таблица

КритерийJWTOAuth 2.0Me2eM 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 достаточно и проще.

Что читать дальше

Поделиться:

Похожие статьи

Пост-мортем: эволюция @me2em/core — от библиотеки идентичности к протоколу с аттестациями

Честный разбор семи дизайн-решений: детерминизм вместо хранения, stateless-сессии, два режима верификации, аттестации вместо ролей, каноничность имён, свежий salt для кэша и запрет zero-fallback — с альтернативами и тем, что мы бы сделали иначе.

M Me2eM Organization
7 мин. чтения
#architecture #cryptography #security

Почему мы не храним ваш seed — и почему это фича, а не недоработка

Seed-фраза — единственный носитель вашей идентичности. Мы не храним её. Почему это осознанное решение, какую цену вы за него платите, и почему эта цена честнее альтернативы.

M Me2eM Organization
7 мин. чтения
#security #identity #architecture

Готовы начать?

Установите пакет, пройдите quick start и за считанные минуты запустите свой первый сценарий.