Token-based аутентификация

Token-based аутентификация основана на передаче клиенту специального маркера (токена), который подтверждает его подлинность при последующих запросах. В отличие от сессионной аутентификации, где состояние хранится на сервере, токены позволяют реализовать stateless-архитектуру, что особенно важно для распределённых систем, edge-приложений и serverless-подхода, на котором построен Fresh.

Ключевые свойства модели:

  • отсутствие серверного состояния сессии
  • масштабируемость без синхронизации хранилищ
  • совместимость с REST и HTTP-кешированием
  • удобство для SPA и SSR одновременно

В экосистеме JavaScript токены чаще всего представлены в виде JWT (JSON Web Token), хотя сама модель не ограничивается только этим форматом.


Особенности Fresh и влияние на аутентификацию

Fresh — фреймворк для Deno с акцентом на:

  • серверный рендеринг без сборки
  • маршрутизацию на основе файлов
  • выполнение кода максимально близко к пользователю
  • минимальное использование клиентского JavaScript

Это накладывает важные ограничения и преимущества при проектировании аутентификации:

  • middleware выполняется на сервере при каждом запросе
  • cookies обрабатываются нативно через Web API
  • нет глобального состояния между запросами
  • безопасность важнее удобства хранения токенов в JS

По этой причине cookie-based token storage является предпочтительным вариантом по сравнению с localStorage.


Структура JWT

JWT состоит из трёх частей, закодированных в Base64URL и разделённых точками:

header.payload.signature

Header Содержит информацию о типе токена и алгоритме подписи:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload Набор утверждений (claims):

  • sub — идентификатор пользователя
  • exp — время истечения
  • iat — время выпуска
  • iss — издатель
  • кастомные поля (роль, права, tenant)

Signature Результат подписи header + payload с использованием секретного ключа или приватного ключа.

В Fresh токены обычно подписываются на сервере с использованием стандартного crypto.subtle.


Процесс аутентификации

1. Вход пользователя

  • клиент отправляет логин и пароль через POST
  • сервер проверяет учётные данные
  • создаётся access token
  • токен отправляется клиенту через Set-Cookie

Пример установки cookie:

ctx.response.headers.set("Set-Cookie",
  "access_token=...; HttpOnly; Secure; SameSite=Strict; Path=/"
);

Критически важные флаги:

  • HttpOnly — токен недоступен из JavaScript
  • Secure — передача только по HTTPS
  • SameSite — защита от CSRF

2. Аутентифицированные запросы

Каждый последующий запрос автоматически содержит cookie. Fresh-middleware перехватывает его до выполнения обработчика маршрута.

export async function handler(req: Request, ctx: FreshContext) {
  const token = getCookies(req.headers).access_token;
}

После извлечения токена:

  • проверяется подпись
  • проверяется срок действия
  • данные пользователя помещаются в ctx.state

Middleware в Fresh

Middleware — ключевая точка реализации token-based аутентификации.

Пример глобального middleware:

export async function handler(req, ctx) {
  const cookies = getCookies(req.headers);
  const token = cookies.access_token;

  if (token) {
    const payload = await verifyJWT(token);
    ctx.state.user = payload;
  }

  return await ctx.next();
}

Преимущества такого подхода:

  • единая точка проверки
  • отсутствие дублирования логики
  • прозрачность для маршрутов

Защита маршрутов

На уровне маршрута проверяется наличие ctx.state.user.

if (!ctx.state.user) {
  return new Response("Unauthorized", { status: 401 });
}

Для сложных сценариев:

  • проверка ролей
  • проверка scope
  • проверка принадлежности ресурса

Access Token и Refresh Token

Для повышения безопасности применяется двухтокенная схема.

Access Token

  • короткий срок жизни (5–15 минут)
  • используется для каждого запроса

Refresh Token

  • длительный срок жизни
  • хранится только в HttpOnly cookie
  • используется для обновления access token

Схема обновления:

  1. Access token истёк
  2. Клиент получает 401
  3. Выполняется запрос /auth/refresh
  4. Сервер проверяет refresh token
  5. Выдаётся новый access token

В Fresh refresh-маршрут реализуется как обычный серверный endpoint без клиентского JS.


Хранение и отзыв токенов

Поскольку JWT stateless, отзыв токена требует дополнительных механизмов:

  • blacklist (Redis, KV)
  • versioning (tokenVersion в базе)
  • короткий срок жизни access token

В Deno-окружении часто используется:

  • Deno KV
  • внешнее key-value хранилище

Пример проверки версии:

if (payload.version !== user.tokenVersion) {
  throw new Error("Token revoked");
}

CSRF и token-based подход

При использовании cookie-based токенов CSRF остаётся актуальной угрозой.

Методы защиты:

  • SameSite=Strict
  • double submit cookie
  • CSRF token в заголовке

Для Fresh оптимален double submit:

  • CSRF-токен хранится в обычной cookie
  • значение дублируется в заголовке запроса

SSR и безопасность

Fresh выполняет серверный рендеринг при каждом запросе. Это позволяет:

  • проверять токен до генерации HTML
  • скрывать защищённые данные
  • отдавать разные версии страницы

Пример:

if (!ctx.state.user) {
  return ctx.render({ user: null });
}

HTML никогда не содержит токен, а только данные, полученные после его проверки.


Ошибки проектирования

Распространённые проблемы:

  • хранение JWT в localStorage
  • длинный срок жизни access token
  • отсутствие проверки exp
  • использование одного секрета для разных сред
  • логирование токенов

В контексте Fresh особенно критично избегать утечек через серверные логи и edge-окружения.


Итоговая архитектура

Корректная token-based аутентификация в Fresh строится на следующих принципах:

  • JWT + HttpOnly cookies
  • глобальный middleware
  • минимальный срок жизни токенов
  • отсутствие клиентского хранения
  • строгая проверка на сервере

Такой подход полностью соответствует философии Fresh и позволяет строить безопасные, масштабируемые JavaScript-приложения без избыточной сложности.