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

Stateless-сессии: авторизация без базы данных

Как подписанный токен заменяет серверную сессию: что внутри, что проверяет верификатор, чем stateless отличается от JWT — и почему ротация jti делает отзыв точечным.

M

Me2eM Organization

4 мин. чтения

Каждая классическая система авторизации хранит сессии. Пользователь логинится — сервер создаёт запись в Redis или PostgreSQL: пользователь X вошёл, права такие-то, истекает тогда-то. Каждый следующий запрос лезет в это хранилище за проверкой. Хранилище становится единственной точкой отказа, объектом для масштабирования и мишенью для атак.

Me2eM работает иначе: сессия — это подписанный токен, который несёт всё необходимое внутри себя. Сервер проверяет подпись и читает данные из токена — без единого обращения к хранилищу. Тот же принцип, что у JWT, но с другой криптографической основой и другой моделью доверия.

Что внутри токена

Сессия создаётся хэндлом — подписывается его ключом:

import { Session } from '@me2em/core';

const session = await Session.create(connector, {
  audience: 'ev-app.com',       // для кого этот токен
  scopes: ['charge:start'],     // что можно делать
  ttl: 1800,                    // на сколько секунд
});

Payload, который подписывается:

interface SessionPayload {
  hId: string;        // кто: base64url публичного ключа хэндла
  hNm: string;        // имя хэндла (каноничное)
  hPath?: string[];   // путь деривации для SubHandle: ['station-001', 'connector-ccs']
  aud: string;        // для кого
  scp: string[];      // права
  exp: number;        // когда истечёт
  iat: number;        // когда выпущен
  jti: string;        // уникальный ID — ключ для отзыва
}

Токен: base64url(payload).base64url(Ed25519-подпись). Всё необходимое для авторизации — внутри подписанного конверта.

flowchart LR
    subgraph Creation ["Создание"]
        H["Handle<br/>приватный ключ"] -->|"подписывает payload"| T["Token<br/>payload.signature"]
    end
    subgraph Verification ["Верификация"]
        V["Сервер"] --> P1["1. Формат и поля"]
        V --> P2["2. Аудитория"]
        V --> P3["3. Время: exp, iat"]
        V --> P4["4. Реконструкция pubkey<br/>по имени из токена"]
        V --> P5["5. Подпись"]
        V --> P6["6. Отзыв — опционально"]
    end
    T -->|"предъявляет"| V

Что проверяет сервер — по шагам

Верификатор не имеет ключа подписанта. Он пересчитывает публичный ключ из имени в токене — это возможно благодаря детерминированной деривации:

// Верификатор держит Identity — пересчитывает ключ хэндла по имени
const handle = await identity.deriveHandle(payload.hNm);
// или для SubHandle — по полному пути:
const sub = await identity.deriveSubHandle(payload.hPath[0], payload.hPath[1]);

Последовательность проверок:

#ПроверкаЧто отклоняет
1Формат: две base64url-части, размер ≤ 4096 байтбитые токены
2Обязательные поля: hId, hNm, aud, scp, exp, iat, jtiнеполные токены
3Аудитория: aud совпадает с ожидаемойтокены другого сервиса
4Время: exp не истёк, iat не в будущем (skew ±30с)протухшие и подделанные «из будущего»
5Реконструкция ключа + проверка подписиподделку payload
6hId совпадает с пересчитанным pubkeyподмену идентичности
7Ревокация: jti не в списке отозванныхпринудительно отозванные сессии

Только после всех семи — сессия считается валидной. Сервер ни разу не обратился к БД.

Отзыв: единственная точка состояния

Stateless не значит «отзыв невозможен». Значит — отзыв опционален и гранулярен.

interface RevocationChecker {
  isRevoked(jti: string): Promise<boolean>;
}

Сервер, который хочет уметь отзывать сессии мгновенно (не дожидаясь TTL), реализует этот интерфейс — например, на Redis SET:

// Отзыв одной сессии (jti — из подписанного payload отзываемого токена):
await redis.sadd('me2em:revoked', jti);

// Проверка — O(1), ~0.1 мс на уровень:
const revoked = await redis.sismember('me2em:revoked', jti);

Компромисс честен: без хранилища отзыва нет мгновенного отзыва (сессии живут до TTL). С хранилищем — мгновенный отзыв есть, но появляется состояние (маленькое, но состояние). Me2eM даёт выбор: stateless-чистота или мгновенный отзыв — приложение решает.

Чем это отличается от JWT

Схожесть поверхностна: оба — подписанные токены с payload. Различия — в модели доверия и основания подписи:

АспектJWTMe2eM Session
Основа подписисерверный секрет (HMAC) или серверный приватный ключключ хэндла — детерминированно выведенный из seed пользователя
Кто создаётсерверклиент — хэндл подписывает сам
Идентичность в токенеsub — строка из БДhId — публичный ключ хэндла, верифицируемый криптографически
ИерархиянетhPath — путь в дереве деривации
Отзыв веткинетаттестационный jti отключает всю ветку (в Mode 2)
Верификация без БДда, но нужен серверный ключда, и серверу не нужен даже секрет — только публичный корень

Ключевое различие — кто владеет подписывающим ключом. В JWT подписывает сервер, значит сервер всемогущ: он может выдать любой токен с любыми правами. В Me2eM подписывает хэндл пользователя — сервер лишь верифицирует, не имея права создавать чужие сессии. Это смещение доверия: от «поверь серверу» к «проверь криптографию».

Одно уточнение к таблице. Всё описанное выше — Mode 1 (verifyStateless): верификатор держит Identity и сам пересчитывает ключи по именам из токена. Строка «серверу не нужен даже секрет» — это Mode 2 (verifyAttested): сервер проверяет цепочку родительских аттестаций против публичного корневого ключа и принуждает гранты на каждой проверке. Полный разбор — в статье об аттестационных цепочках.

Авто-renewal: сессия без логина

Токен короткоживущий (≤ 1 час — рекомендация). Но пользователю нельзя требовать логин каждый час. Решение — авто-renewal:

sequenceDiagram
    participant App as Приложение
    participant K as Ключ хэндла
    App->>K: Session.create, ttl 3600
    K-->>App: сессия, exp = now + 3600
    Note over App: через половину TTL
    App->>K: Session.create — новый jti
    K-->>App: свежая сессия
    Note over App: цикл повторяется,<br/>пользователь не замечает

В @me2em/react это встроено в useSession: авто-renewal на половине TTL, очистка таймеров при unmount, ротация jti при каждом продлении. Пользователь работает неделями, не вводя seed — сессии обновляются сами, а TTL каждого токена остаётся коротким.

Итог

СвойствоКак достигается
Нет БД для верификациивсе данные в подписанном payload
Верификация без серверного секретареконструкция pubkey из имени (детерминизм)
Мгновенный отзывjti + RevocationChecker — опционально
Короткие TTL без UX-ценыавто-renewal на половине TTL
Иерархия в токенеhPath — путь в дереве деривации
Отзыв ветки целикоматтестационный jti (Mode 2)

Stateless-сессия — не изобретение Me2eM: это идея, старая как PGP. Но соединение stateless-токенов с детерминированной деривацией и аттестационными цепочками даёт свойство, которого нет в классических схемах: сервер без единой записи в базе — а в Mode 2 и без единого секрета — проверяет не только «кто ты», но и «кто разрешил тебе существовать».

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

Поделиться:

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

SSO без базы пользователей: seed-фраза как аккаунт платформы

Паттерн Multi-App SSO на Me2eM: один инстанс, много приложений, ноль пользовательских таблиц. Per-app персоны, аттестационные цепочки вместо IdP, HttpOnly-сессии и бан без базы данных — по корню идентичности.

M Me2eM Organization
8 мин. чтения
#identity #sessions #architecture

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

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

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

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

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

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

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

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