Зарегистрируйтесь, чтобы продолжить обучение

Аутентификация JS: REST API (Fastify)

В обычных веб-приложениях, работающих в браузере, аутентификация обычно осуществляется с помощью cookies, в которых хранится сессионный ключ. Браузер автоматически отправляет эти cookies при загрузке страниц, поддерживая состояние аутентификации пользователя на разных страницах сайта. Однако в API, особенно RESTful API, такие механизмы отсутствуют, поэтому аутентификация там работает немного иначе. В этом уроке мы подробно рассмотрим, как реализовать аутентификацию с помощью API-ключей и JWT-токенов — двух распространенных методов для обеспечения безопасности API.

Аутентификация в веб-приложениях

Прежде чем перейти к аутентификации в API, кратко напомним, как работает аутентификация в традиционных веб-приложениях. Когда пользователь входит в систему, сервер создает сессию и отправляет идентификатор сессии обратно в браузер, обычно храня его в cookie. При последующих запросах браузер отправляет этот cookie обратно серверу, позволяя серверу распознать пользователя и получить данные его сессии.

Этот механизм опирается на автоматическую обработку cookies браузером и способность сервера поддерживать состояние сессии. Хотя это хорошо работает для веб-приложений, это не переносится напрямую на API, особенно когда клиентами являются не браузеры, а другие сервисы или мобильные приложения.

Api Keys

Этот метод аутентификации в API предполагает, что каждое приложение или клиент получает уникальный ключ, который используется для доступа к API. API-ключ обычно выдается разработчику при регистрации приложения в сервисе, предоставляющем API.

Postman API-ключи

API-ключ — это уникальный идентификатор, назначаемый клиенту, который позволяет серверу распознать клиента, делающего запрос. Клиент включает API-ключ в каждый запрос к API, обычно через заголовок или как параметр запроса. Сервер затем валидирует API-ключ, сверяя его со своими записями, чтобы аутентифицировать клиента.

Процесс аутентификации с API-ключами

  • Запрос доступа клиентом: Клиент запрашивает API-ключ у поставщика сервиса.
  • Выдача API-ключа сервисом: Сервис генерирует уникальный API-ключ и предоставляет его клиенту.
  • Запросы клиента к API: Клиент включает API-ключ в каждый запрос к API. Обычно в виде заголовка. У такого заголовка нет стандартного имени и каждый сервис придумывает что-то свое, например, X-API-KEY.
  • Валидация API-ключа сервером: Сервер проверяет API-ключ, сверяя его со своими записями для аутентификации клиента.
  • Ответ сервера: Если API-ключ валиден, сервер обрабатывает запрос и отправляет ответ.

Пример того, как может выглядеть API-запрос с API-ключом в заголовке:

GET /resource HTTP/1.1
Host: api.hexlet.io
X-API-KEY: <api-key>

Многие сервисы позволяют генерировать несколько API-ключей для разных целей, повышая безопасность путем ограничения области действия каждого ключа. Например, вы можете иметь отдельные ключи для разработки, тестирования и продакшн-среды. Продвинутые системы позволяют связывать ключи с определенными правами доступа, контролируя доступ к разным частям API.

На практике API-ключ должен храниться в секрете, как пароль. Если кто-то получит доступ к вашему API-ключу, он сможет делать запросы к API от вашего имени.

Какие проблемы не решают API-ключи

  • Проблемы масштабируемости: Серверу необходимо обращаться к базе данных для проверки API-ключа при каждом запросе, что может стать узким местом в системах с высокой нагрузкой или в микросервисной архитектуре.
  • Отсутствие контекста пользователя: API-ключи обычно идентифицируют приложение, а не пользователя, что затрудняет выполнение действий от имени конкретных пользователей, если это подразумевается в системе.
  • Ограниченная безопасность: API-ключи не предоставляют детального контроля доступа или аутентификации на уровне пользователя.
  • Сложность управления: Ротация API-ключей и управление их истечением требует дополнительного администрирования.

Из-за этих ограничений, особенно в системах, требующих аутентификации конкретных пользователей или распределенных архитектур, JWT-токены часто являются лучшим выбором.

JWT Token

JWT (JSON Web Token) — это компактный и URL-безопасный способ передачи информации между сторонами в виде JSON-объекта. Они предназначены для передачи информации безопасно и целостно, поскольку могут быть подписаны или зашифрованы.

Структура JWT

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

  • Header (заголовок): содержит метаданные о типе токена и алгоритме подписи.
  • Payload (полезная нагрузка): содержит утверждения (claims) или данные, которые вы хотите передать.
  • Signature (подпись): используется для проверки целостности токена.
# Компоновка. Каждая часть кодируется с помощью Base64URL.
xxxxx.yyyyy.zzzzz

# Конкретный пример
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNzI2MjQ5NjIzfQ.pASCTRgva-EyeCwJYrVWUdxdG2PPb8tRH885A_PxaUg

Как работают JWT-токены

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

Для получения jwt-токена, клиент должен выполнить аутентификацию через специальный эндпоинт, который вернет токен. Затем, при выполнении запросов к защищенным маршрутам клиент должен включать JWT-токен в каждый запрос. Обычно это делается через заголовок Authorization с использованием схемы Bearer:

GET /users/1 HTTP/1.1
Host: api.example.com
Authorization: Bearer <your-jwt-token>

Когда токен истекает, клиент должен получить новый токен, обычно повторно аутентифицируясь или используя механизм обновления токена.

Преимущества JWT-токенов

JWT-токен это строка, внешне похожая на API-ключ, но в отличие от ключа, в нее зашифрована полезная информация, например, идентификатор клиента и набор допустимых действий в системе. Благодаря этому JWT позволяет избежать необходимости хранить сессии на сервере и обеспечивает гибкость в распределенных системах, где API могут быть вызваны из различных источников.

  • Безсессионность: Нет необходимости хранить данные сессии на сервере.
  • Масштабируемость: Сокращает обращения к базе данных, улучшая производительность в распределенных системах.
  • Гибкость: Можно включать различные утверждения и устанавливать истечение срока действия токена.

Использование JWT-токенов в Fastify

Работа с JWT-токенами в Fastify автоматизирована благодаря наличию готовой интеграции. Поэтому нам не придется вникать во все тонкости связанные с созданием, расшифровкой и передачей токенов по HTTP. Вы всегда сможете это сделать при желании, а здесь мы сосредоточимся на том, как все подключить и настроить для работы. Начнем с установки пакета:

npm install @fastify/jwt

Далее зарегистрируем плагин и настроим его в вашем приложении. Создайте файл plugins/jwt.js:

import fp from 'fastify-plugin'
import jwtPlugin from '@fastify/jwt'

export default fp(async (fastify) => {
  fastify.register(jwtPlugin, {
    // секрет используемый для шифрования
    // правильно передавать через переменные окружения
    // https://github.com/fastify/fastify-env
    secret: 'supersecret',
  })
  fastify.decorate('authenticate', async function (request, reply) {
    try {
      await request.jwtVerify()
    }
    catch (err) {
      reply.send(err)
    }
  })
})

Во время настройки создается метод-декоратор authenticate(), который валидирует и расшифровывает jwt-токен. Этот метод автоматически выполняет аутентификацию и извлекает данные из токена помещая их в объект запроса.

Если нам нужна только аутентификация, то достаточно передать этот метод в хук onRequest

fastify.get(
  '/users/:id',
  {
    onRequest: [fastify.authenticate],
  },
  async (request) => {
    const user = await db.query.users.findFirst({
      where: eq(schemas.users.id, request.params.id),
    })
    fastify.assert(user, 404)
    return user
  },
)

Если нужны данные, то после аутентификации они доступны в request.user

  fastify.post(
    '/courses',
    {
      onRequest: [fastify.authenticate],
    },
    async (request, reply) => {
      const data = request.body
      // Данные пользователя извлеченные из jwt-токена
      body.creatorId = request.user.id

      const [course] = await db.insert(schemas.courses)
        .values(body)
        .returning()

      return reply.code(201)
        .send(course)
    },

И последнее, для работы с jwt-токенами понадобится эндпоинт выдачи токенов. Обычно это происходит после успешной авторизации. В примере ниже для этого создается адрес /tokens внутри которого проверяется пользователь, запросивший токен. Затем формируется сам токен с помощью метода fastify.jwt.sign в который передается объект с данными для шифрования. Именно эти данные, потом оказываются внутри request.user.

import { schema } from '../../schema.js'

export default async function (fastify) {
  const db = fastify.db

  fastify.post(
    '/tokens',
    async (request, reply) => {
      const client = await db.query.users.findFirst({
        // Добавить проверку пароля
        where: eq(schemas.users.email, request.body.email),
      })
      fastify.assert.ok(client, 404)
      const token = fastify.jwt.sign(
        { id: user.id, email: user.email },
        { expiresIn: '1h' }, // время протухания
      )
      return reply.code(201)
        .send({ token })
    },
  )
}

Пример того, как это работает:

# Данные взяты из сидов
curl -XPOST localhost:3000/api/tokens \
  -H "Content-Type: application/json" \
  -d '{ "email": "support@hexlet.io", "password": "some secret password" }'

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNzI2MjQ5NjIzfQ.pASCTRgva-EyeCwJYrVWUdxdG2PPb8tRH885A_PxaUg"}

Вопросы безопасности

  • Управление секретом: Секретный ключ для подписания токенов должен храниться в безопасности. Не хардкодьте его; используйте переменные окружения.
  • Хранение токена: Клиенты должны безопасно хранить токены, особенно в браузерных приложениях, где токены могут быть уязвимы для XSS-атак.
  • HTTPS: Всегда используйте HTTPS для предотвращения перехвата токена через атаки типа "человек посередине".

Сравнение API-ключей и JWT-токенов

Характеристика API-ключи JWT-токены
Идентифицируют Приложение/Клиента Пользователя/Клиента
Сессионность Сессионные (требуют проверки в БД) Безсессионные (не требуют проверки в БД)
Масштабируемость Менее масштабируемы Более масштабируемы
Безопасность Менее безопасны Более безопасны (подписанные токены)
Контроль доступа Ограниченный Гибкий (на основе утверждений)
Истечение срока действия Ручное Встроенное

Когда использовать каждый метод

  • API-ключи: Подходят для простых приложений, где нужно идентифицировать клиента, но не отдельных пользователей, или когда API потребляется серверными приложениями.
  • JWT-токены: Идеальны, когда требуется аутентификация на уровне пользователя, безсессионность и масштабируемость, например, в микросервисной архитектуре или при разработке API для мобильных приложений.

Самостоятельная работа

  1. Добавьте аутентификацию с JWT в приложение. Добавьте во все роуты аутентификацию
  2. Добавьте маршрут POST /tokens для генерации токена, как описано в уроке
  3. Запушьте изменения в репозиторий

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff